/* ============================================================
   File:        site.css
   Project:     StudioFlow (Grace Aerobics & Fitness Center)
   Developer:   Janvi Vakhariya — V Techno Labs
   Created:     2026-08-17  (updated 2026-09-09)
   Purpose:     StudioFlow design system — teal/amber on navy-slate, mobile-first app shell.
   Copyright:   © 2026 V Techno Labs. All rights reserved.
                Proprietary software developed by V Techno Labs.
   ============================================================ */

/* ---------- Palette ----------
   Professional fitness theme: cool-gray surfaces, teal actions, amber for the
   one call-to-action per screen, deep navy-slate chrome. Every colour in the
   app comes from these variables -- change them here and the whole PWA follows.
   Readability rule: white on teal/navy/red, dark on amber and on light gray. */
:root {
  /* ---------------------------------------------- THE app background
     The image itself is NOT a variable. It is written once, literally, in the
     .sf-app .sf-frame rule further down -- the single rule that paints it --
     so there is one place to look and one place to change. It used to be a
     token here, read by a pseudo-element down there, and splitting the
     declaration across two places is part of how it came to be painted on a
     layer the body covered without anyone noticing.

     THERE IS ONLY ONE PAGE BACKGROUND IMAGE IN THE APP. Every module sheet
     used to carry its own -- stripes on Members, mats on Batches, a tiled
     ball-pit on Plans, nothing at all on Users -- which is how one screen
     ended up tiled while another ended up plain. They are all gone.

     Only the scrim over it is a token, because two rules read it: this one and
     the contrast note beside --muted. */

  /* The scrim between the photo and the content, and the reason the pattern
     reads as texture rather than as a picture competing with the words.

     .86 white leaves the image at 14% intensity -- the top of the 10-15% band
     this ground is meant to sit in. It is a scrim ON TOP of the photo, not
     opacity on the frame: opacity on the frame would fade the content with it.
     The cards above are solid white, never translucent, so nothing inside a
     card is affected at all.

     MEASURED against the shipped file, not guessed. The darkest pixel of
     balls.png under this scrim is rgb(227,221,227). Against it:
         --text  #1E293B   10.99:1
         --muted #556072    4.78:1   (see the note on --muted below)
     Both clear the 4.5:1 minimum. Raise this number toward 1 for more
     contrast; lowering it below .86 pushes the footer credit under 4.5:1,
     which is the one piece of text that sits on the ground rather than on a
     card. Legibility wins over the photo. */
  --sf-bg-scrim: rgba(255, 255, 255, .86);

  /* ------------------------------------------------- the app frame
     StudioFlow is a phone app. On a laptop it is drawn as a phone-width column
     with a neutral surround, so it reads as an app rather than as a website
     that happens to be narrow. Below this width the frame is simply 100% and
     nothing about a phone changes.

     One number: change --frame-w and the frame, both fixed bars and the
     background all follow it. */
  --frame-w: 480px;

  /* The area outside the frame on a wide screen. Deliberately understated -- a
     shade darker than the app ground so the frame reads as a panel, not as a
     second colour competing with the design inside it. */
  --surround: #E2E6EB;

  --bg:            #F4F6F8;   /* page background -- light cool gray */
  --card:          #FFFFFF;   /* cards and surfaces */

  --primary:       #14B8A6;   /* brand teal -- decorative fills with no text on them */

  /* White on #14B8A6 measures only 2.49:1, well under the 4.5 WCAG minimum, so
     anything that carries white text uses this deeper teal instead: 5.47:1. */
  --primary-strong: #0F766E;  /* buttons, active tab pill, badges */
  --primary-dark:  #115E59;   /* pressed / hover state -- 7.58:1 */
  --primary-text:  #FFFFFF;   /* text ON the deeper teal */

  --accent:        #F59E0B;   /* amber -- key CTAs (Renew / Pay), highlights */
  --accent-dark:   #D97706;   /* pressed / hover state */
  --accent-text:   #1E293B;   /* text ON amber -- dark, never white */

  --header:        #0F172A;   /* deep navy-slate -- app bar and bottom nav */
  --header-text:   #FFFFFF;   /* text and icons on the dark chrome */

  --text:          #1E293B;   /* primary text -- near-black slate */
  /* Secondary text. Deepened from #64748B, which measured only 3.51:1 against
     the darkest scrimmed pixel of the ground -- under the 4.5:1 minimum for the
     footer credit and any other muted line that sits on the ground rather than
     inside a card. #556072 measures 4.78:1 there and 6.36:1 on white, so it
     passes in both places. Deepening the ink was the fix rather than putting a
     panel behind the text. */
  --muted:         #556072;   /* secondary text */
  --border:        #E2E8F0;   /* borders and dividers */

  --success:       #22C55E;   /* active / paid */
  --danger:        #EF4444;   /* expired / overdue */

  /* Tinted surfaces derived from the status colours, for banners and pills. */
  --success-bg:    #ECFDF5;
  --success-line:  #A7F3D0;
  --success-text:  #047857;

  --danger-bg:     #FEF2F2;
  --danger-line:   #FECACA;
  --danger-text:   #B91C1C;

  --warn-bg:       #FFFBEB;
  --warn-line:     #FDE68A;
  --warn-text:     #92400E;

  /* Neutral fill for hover states on light surfaces. */
  --surface-2:     #EEF2F6;
  --violet-bg:     #F3EEFD;   /* tint behind the "present" tile */
  --violet-text:   #5B21B6;   /* 7.31:1 on that tint */

  /* The studio's own colours, taken from the sign on the wall. The red is the
     monogram; the pink is the owner's signature above it. Both are measured on
     the light sign-in ground: red 5.94:1, pink 6.31:1 -- well past the 4.5
     minimum, because a brand that cannot be read is not a brand. */
  --brand-red:     #C41230;
  --brand-plate:   #F2EFEA;
  --brand-pink:    #C2185B;

  /* One filled colour per class type, so Aerobics, Yoga and Dance are told
     apart at a glance. THIS IS THE ONLY PLACE THE THREE ARE WRITTEN DOWN.
     Everything reads them from here -- the activity badge, the plan and batch
     lists, the filter chips, the group rules, and the dashboard's time chips
     via --sf-dash-aer / -yog / -dan in dashboard.css -- so a badge and a chip
     for the same class type can never drift apart.

     Restated 2026-08-31 to the dashboard design's values: teal #17A89A, amber
     #F08A1D, violet #9D76E8.

     Each carries its own text colour, because all three of these fills are
     lighter than the set they replaced. Contrast measured against each fill:
     teal 4.95:1, amber 5.82:1, violet 5.05:1 -- all clear of the 4.5:1 minimum.
     The violet's ink changed with it: the new #9D76E8 is light enough that white
     would sit at 3.39:1, which fails at the .68rem the badge is set in, so it
     takes a dark plum instead. The three still differ in lightness, so they stay
     distinguishable without relying on hue alone. */
  /* Frozen sits deliberately outside the green/red pair: a paused membership
     is neither running nor finished. Slate blue-grey reads as "on hold" without
     competing with the amber call-to-action. White on it measures 5.92:1. */
  --frozen:        #475569;
  --frozen-text:   #FFFFFF;
  --frozen-bg:     #EEF2F6;
  --frozen-line:   #CBD5E1;

  --activity-aerobics:      #17A89A;
  --activity-aerobics-text: #1E293B;
  --activity-yoga:          #F08A1D;
  --activity-yoga-text:     #1E293B;
  --activity-dance:         #9D76E8;
  --activity-dance-text:    #241338;

  --radius:        12px;
  --shadow:        0 1px 2px rgba(15, 23, 42, .04), 0 4px 12px rgba(15, 23, 42, .06);
  --tap:           52px;      /* comfortably above the 44px minimum */

  --appbar-h:      56px;
  --tabbar-h:      64px;
}

/* ---------- Base ---------- */
html {
  font-size: 16px;              /* >=16px stops iOS zooming on input focus */
  -webkit-text-size-adjust: 100%;
}

.sf-body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: var(--bg);
  color: var(--text);
  overflow-x: hidden;           /* guard against a stray wide child */
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}

/* Nothing may outgrow the viewport. */
img, svg, video, canvas, iframe { max-width: 100%; height: auto; }

/* ---------- App shell ----------
   Fixed bar at the top, fixed tabs at the bottom, content scrolling between.
   The body carries the padding so nothing hides underneath either bar. */
/* The surround. It lives on <html> for a reason: when only <body> carries a
   background the browser propagates it to the canvas and paints it across the
   entire window, so narrowing the body alone would not have contained the
   background at all. Giving <html> its own stops that propagation. */
html {
  background-color: var(--surround);
}

.sf-app {
  margin: 0;
  min-height: 100vh;

  /* TRANSPARENT, EXPLICITLY, AND THIS MATTERS.
     Bootstrap loads before this file and sets `body { background-color: #fff }`
     from --bs-body-bg. Leaving it alone is what made the app background
     invisible on every signed-in page: the ground was painted on a z-index:-1
     pseudo-element, and a negative-z-index child paints BELOW the in-flow
     background of the body it sits in, so an opaque white body covered it
     completely. It also meant the desktop surround was Bootstrap's white
     rather than --surround, which <html> carries.
     Stating transparent here is what lets the frame's ground and the surround
     both show. Do not remove it, and do not give this element a colour. */
  background-color: transparent;

  color: var(--text);
  overflow-x: hidden;           /* guard against a stray wide child */
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
}

/* ------------------------------------------------------------- the app frame
   Wraps the fixed header, the scrolling content and the tab bar, so all three
   are bounded by the same column. Below --frame-w it is 100% wide and a phone
   sees exactly what it saw before: no frame, no surround, edge to edge. */
.sf-frame {
  position: relative;

  /* z-index: 0 makes this element a STACKING CONTEXT, and that is load-bearing.
     It does two jobs at once:
       the frame's own background paints above the body's, because a positioned
       element is painted after in-flow block backgrounds;
       the scrim below can then sit at z-index -1 INSIDE this context, which
       puts it above this element's background and below every piece of
       content -- exactly where a scrim belongs.
     z-index alone does not change the containing block of a fixed descendant,
     so the app bar and the tab bar go on positioning against the viewport
     exactly as before. Only a transform, filter or perspective would move them,
     and those live on the bars themselves, not here. */
  z-index: 0;

  min-height: 100vh;
  overflow-x: hidden;

  width: 100%;
  max-width: var(--frame-w);
  margin: 0 auto;

  /* The bars are fixed and out of flow, so the frame reserves their space. */
  padding-top: var(--appbar-h);
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

/* THE application background, declared ONCE, for every signed-in page.

   ON THE FRAME ITSELF, not on a pseudo-element behind it. That is the fix: the
   ground used to be painted on .sf-frame::before at z-index -1, and a negative
   z-index child paints BELOW the in-flow background of its ancestors. Bootstrap
   loads before this file and paints the body opaque white, so the image was
   drawn and then covered on every single signed-in screen. Painting it on the
   frame -- a positioned element -- puts it above the body's background, where
   nothing in the shell can cover it.

   Not on html and not on body, so on a wide screen it is bounded by the
   phone-width column instead of spanning the browser window.

   cover + no-repeat is what stops it tiling; center keeps the crop honest on
   any aspect; fixed holds it still while the content scrolls, so it reads as
   the room rather than as part of the page.

   Swap the photo by changing the one url() below. Nothing else moves.

   Scoped to .sf-app on purpose. The login screen is framed too, but it paints
   its own crimson field -- and that field is a translucent radial gradient, so
   an unscoped rule would show the studio photo through it and quietly change a
   screen that is meant to stay exactly as it is. */
.sf-app .sf-frame {
  /* The ground under the photo, so nothing shows white while it loads. */
  background-color: var(--bg);

  background-image: url("/img/balls.png");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
}

/* THE SCRIM -- an overlay layer, never opacity on the frame.

   Opacity on the frame would fade the text with the photo. This is a flat
   translucent white sheet laid OVER the image and UNDER everything else, which
   is what brings the pattern down to roughly 14% and leaves every word at full
   strength. Cards above it stay solid.

   z-index -1 inside the frame's own stacking context: above the frame's
   background, below all of its content. inset: 0 covers the whole scrollable
   frame, so a long page is scrimmed to its end. */
.sf-app .sf-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-color: var(--sf-bg-scrim);
}

/* A quiet edge so the column reads as a panel on a wide screen. Both are
   suppressed below the frame width, where there is no surround to sit against. */
@media (min-width: 481px) {
  .sf-frame {
    box-shadow: 0 0 0 1px rgba(43, 42, 31, .10), 0 18px 46px -24px rgba(43, 42, 31, .45);
  }
}

/* Inner pages hide the tabs, so they reclaim most of that space -- but not all
   of it. A sticky action bar sits at the bottom of the content area, and on a
   phone with a gesture bar it needs the safe-area inset underneath it. */
.sf-app-notabs {
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 1rem);
}

/* --- Top app bar --- */
.sf-appbar {
  position: fixed;
  top: 0;

  /* Centred and width-limited rather than left:0/right:0. A fixed element is
     positioned against the VIEWPORT, not against its parent, so inside a
     phone-width frame it would still have spanned the whole laptop window. */
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--frame-w);

  z-index: 20;
  height: var(--appbar-h);
  background-color: var(--header);
  border-bottom: 1px solid color-mix(in srgb, var(--header-text) 12%, transparent);
  display: flex;
  align-items: center;
}

/* Three columns keep the title optically centred whether or not a back
   arrow is present. */
.sf-appbar-inner {
  width: 100%;
  max-width: 640px;
  height: 100%;
  margin: 0 auto;
  padding: 0 .25rem;
  display: grid;
  grid-template-columns: 48px 1fr 48px;
  align-items: center;
}

.sf-appbar-title {
  margin: 0;
  text-align: center;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--header-text);    /* white on navy -- never dark text */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sf-icon-btn {
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 12px;
  background: transparent;
  color: var(--header-text);   /* white on the navy bar -- dark here is invisible */
  text-decoration: none;
  cursor: pointer;
}
.sf-icon-btn:hover, .sf-icon-btn:focus { background: color-mix(in srgb, var(--header-text) 14%, transparent); color: var(--header-text); }
.sf-icon-btn svg { width: 24px; height: 24px; }

/* Keeps the grid column occupied when there is no back arrow. */
.sf-icon-spacer { width: 48px; height: 48px; }

.sf-appbar-form { margin: 0; justify-self: end; }

/* --- Bottom tab bar --- */
.sf-tabbar {
  position: fixed;
  bottom: 0;

  /* Same reason as the app bar above: fixed is measured from the viewport, so
     the bar has to be centred and capped to line up with the frame. */
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--frame-w);

  z-index: 20;
  background-color: var(--header);
  border-top: 1px solid color-mix(in srgb, var(--header-text) 12%, transparent);
  padding-bottom: env(safe-area-inset-bottom, 0px);   /* iPhone home indicator */
}

/* One column per tab, however many there are. It was repeat(4, 1fr), which is
   right for the instructor's three tabs and wrong for the owner's five: the
   fifth wrapped onto a second row and the bar became 128px tall, sitting over
   the content. grid-auto-flow counts the children instead of assuming. */
.sf-tabbar-inner {
  max-width: 640px;
  margin: 0 auto;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}

.sf-tab {
  min-height: var(--tabbar-h);  /* 64px -- well past the 48px minimum */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: .15rem;
  padding: .35rem .2rem;
  color: color-mix(in srgb, var(--header-text) 62%, transparent);
  font-size: .72rem;
  font-weight: 600;
  text-decoration: none;
}
.sf-tab:hover, .sf-tab:focus { color: var(--header-text); }
/* 24px, matching the supplied tile artwork on the other tabs. It was 22px,
   which left the bar mixing two glyph sizes once the tiles arrived. */
.sf-tab svg { width: 24px; height: 24px; }

/* The active tab gets a teal pill behind its icon and a full-white label. */
.sf-tab-icon {
  width: 46px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
}
.sf-tab-active { color: var(--header-text); }
.sf-tab-active .sf-tab-icon {
  background-color: var(--primary-strong);
  color: var(--primary-text);
}

/* ---------- Content area ----------
   Sits between the fixed app bar and the fixed tab bar and scrolls on its own. */
.sf-content {
  width: 100%;
  max-width: 640px;             /* single column on phones, centred on desktop */
  margin: 0 auto;
  padding: 1rem;
}

/* Credit footer: two centred lines in normal page flow, never sticky and never
   boxed. The container's own bottom padding is what clears the tab bar, so this
   only needs breathing room above it. */
.sf-footer {
  flex-shrink: 0;
  text-align: center;
  padding: 20px 1rem 8px;
}

/* The tagline. Amber, so it reads as the studio's voice rather than as chrome. */
.sf-footer-tag {
  margin: 0 0 .2rem;
  color: var(--accent-dark);
  font-size: 13px;
  font-weight: 600;
  line-height: 1.4;
}

/* The credit. Quiet by design -- it must not compete with the page above it.
   Rendered as a link once a number is configured, otherwise as plain text; both
   look identical, so the footer never shifts when the number is filled in. */
.sf-footer-credit {
  display: block;
  margin: 0;
  color: var(--muted);
  font-size: 11px;
  font-weight: 400;
  line-height: 1.5;
  text-decoration: none;
  /* Wraps between words on a very narrow screen rather than overflowing. */
  overflow-wrap: anywhere;
}
a.sf-footer-credit:hover,
a.sf-footer-credit:focus-visible { text-decoration: underline; }

/* The name lifts gently out of the line without shouting. */
.sf-footer-name { font-weight: 500; color: var(--text); }

/* ---------- Cards ---------- */
.sf-card {
  background-color: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem;
  margin-bottom: 1rem;
}

.sf-welcome { border-top: 4px solid var(--primary); }

/* Who you are, what you are, what day it is -- one small line above the cards.
   Wraps between the parts on a narrow phone rather than overflowing, and the
   separators disappear when it does, so a wrapped line never starts with a dot. */
.sf-signedin {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 .35rem;
  margin: 0 0 1rem;
  font-size: .78rem;
  line-height: 1.5;
  color: var(--muted);
}
.sf-signedin strong { color: var(--text); font-weight: 700; }
.sf-signedin-sep { color: var(--border); }

.sf-h1 { font-size: 1.4rem; font-weight: 700; margin: 0 0 .5rem; }
.sf-h2 { font-size: 1.1rem; font-weight: 700; margin: 0 0 .5rem; }
.sf-muted { color: var(--muted); }
.sf-mb-0 { margin-bottom: 0; }

.sf-eyebrow {
  text-transform: uppercase;
  letter-spacing: .06em;
  font-size: .72rem;
  font-weight: 600;
  color: var(--muted);
  margin: 0 0 .35rem;
}

.sf-badge {
  display: inline-block;
  background-color: var(--primary-strong);
  color: var(--primary-text);
  border-radius: 999px;
  padding: .3rem .85rem;
  font-size: .8rem;
  font-weight: 700;
}

/* ---------- Buttons ----------
   Dark text on yellow: the gold is bright, so white text on it would fail
   contrast badly. Dark-on-gold reads cleanly in sunlight too. */
.sf-btn {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: 0 1rem;
  border: none;
  border-radius: 10px;
  background-color: var(--primary-strong);
  color: var(--primary-text);
  font-size: 1.05rem;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  line-height: var(--tap);
  cursor: pointer;
}
.sf-btn:hover, .sf-btn:focus { background-color: var(--primary-dark); color: var(--primary-text); }

.sf-btn-ghost {
  background-color: transparent;
  border: 1px solid var(--border);
  color: var(--text);
  font-weight: 600;
}
.sf-btn-ghost:hover, .sf-btn-ghost:focus { background-color: var(--surface-2); color: var(--text); }

/* ---------- Forms ---------- */
.sf-field { margin-bottom: 1rem; }

.sf-label {
  display: block;
  margin-bottom: .35rem;
  font-size: .9rem;
  font-weight: 600;
  color: var(--text);
}

.sf-input {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: .75rem .9rem;
  font-size: 1rem;              /* 16px -- keeps iOS from zooming in */
  color: var(--text);
  background-color: var(--card);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.sf-input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 30%, transparent);
}

.sf-field-error {
  display: block;
  margin-top: .35rem;
  font-size: .85rem;
  color: var(--danger-text);
}

.sf-alert {
  background-color: var(--danger-bg);
  border: 1px solid var(--danger-line);
  color: var(--danger-text);
  border-radius: 10px;
  padding: .75rem .9rem;
  font-size: .92rem;
  margin-bottom: 1rem;
}

/* ---------- Sign-in screen ---------- */
/* The login screen paints its own crimson field. It goes on the FRAME, not on
   the body: on a wide screen a full-width body would paint the surround crimson
   and defeat the whole point of framing the app. */
.sf-body-auth { background-color: transparent; }
.sf-body-auth .sf-frame { background-color: var(--bg); }

.sf-auth-main {
  flex: 1 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem 1rem;
}

.sf-auth-wrap { width: 100%; max-width: 400px; }

.sf-auth-head { text-align: center; margin-bottom: 1.5rem; }

/* Holds the G monogram. No fill and no rounded tile -- the mark is the shape,
   and a coloured square behind it only fights the red. */
.sf-logo {
  width: 104px;
  height: 104px;
  margin: 0 auto .5rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.sf-logo-mark { width: 100%; height: 100%; display: block; }

.sf-auth-title { font-size: 1.5rem; font-weight: 700; margin: 0 0 .2rem; }
.sf-auth-sub   { color: var(--muted); font-size: .95rem; margin: 0; }

.sf-auth-card { margin-bottom: 1rem; }



/* ---------- Password show/hide toggle ----------
   The button sits inside the input's right edge; .sf-input-has-toggle reserves
   the space so the dots never slide underneath it. */
.sf-input-wrap {
  position: relative;
  display: block;
}

.sf-input-has-toggle { padding-right: 3.25rem; }

.sf-eye {
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  width: 42px;                  /* comfortably past the 40px tap minimum */
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.sf-eye:hover { color: var(--text); background: var(--surface-2); }
.sf-eye:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  color: var(--text);
}
.sf-eye svg { width: 20px; height: 20px; display: block; }

/* ---------- User management ---------- */
.sf-page-head { margin: 0 0 1rem; }
.sf-mb-3 { margin-bottom: 1rem; }

.sf-flash {
  background: var(--success-bg);
  border: 1px solid var(--success-line);
  color: var(--success-text);
  border-radius: 10px;
  padding: .8rem .9rem;
  font-size: .92rem;
  margin-bottom: 1rem;
}
.sf-flash-error {
  background: var(--danger-bg);
  border-color: var(--danger-line);
  color: var(--danger-text);
}

.sf-user-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .75rem;
  flex-wrap: wrap;
}
/* Without min-width:0 a flex child refuses to shrink below its content, so a
   long name shoves whatever sits beside it off the edge of the card. */
.sf-user-top > * { min-width: 0; }
.sf-user-name { font-size: .9rem; margin: .15rem 0 0; }

.sf-user-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: .85rem 0;
  font-size: .875rem;
}

/* Status pill: green for active, gray for retired accounts. */
.sf-pill {
  flex-shrink: 0;
  border-radius: 999px;
  padding: .25rem .7rem;
  font-size: .75rem;
  font-weight: 700;
  white-space: nowrap;
}
.sf-pill-on  { background: var(--success-bg); color: var(--success-text); }
.sf-pill-off { background: var(--surface-2); color: var(--muted); }

/* Row actions wrap onto their own lines on a narrow phone. */
.sf-actions {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  border-top: 1px solid var(--border);
  padding-top: .85rem;
}
/* A form sitting beside a name that truncates. flex:none is what stops it
   being squeezed: without it a long member name shrank the form until the
   button inside wrapped its own label to "Ad / d". The name column has
   min-width:0 and is the thing that should give way, not the action. */
.sf-inline-form { margin: 0; flex: none; }

.sf-btn-sm {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;              /* full tap target even though the button is small */
  padding: 0 .9rem;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--card);
  color: var(--text);
  font-size: .9rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;           /* a one-word label never breaks across lines */
  cursor: pointer;
}
.sf-btn-sm:hover, .sf-btn-sm:focus { background: var(--surface-2); color: var(--text); }

.sf-btn-sm-danger { color: var(--danger-text); border-color: var(--danger-line); }
.sf-btn-sm-danger:hover, .sf-btn-sm-danger:focus { background: var(--danger-bg); color: var(--danger-text); }

.sf-btn-sm-disabled {
  opacity: .45;
  cursor: not-allowed;
  background: var(--surface-2);
}

/* Native select, sized to match .sf-input. */
.sf-select {
  appearance: none;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2364748B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .85rem center;
  background-size: 18px 18px;
  padding-right: 2.75rem;
}

.sf-hint {
  font-size: .82rem;
  color: var(--muted);
  margin: .4rem 0 0;
}

.sf-check {
  display: flex;
  align-items: center;
  gap: .6rem;
  min-height: 44px;
  font-weight: 600;
  cursor: pointer;
}
.sf-check-box { width: 22px; height: 22px; accent-color: var(--primary); }

/* Amber flash: used for the "you must change your password" notice. */
.sf-flash-warn {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  color: var(--warn-text);
}

/* ---------- Device slots ---------- */
.sf-slots { border-top: 4px solid var(--primary); }

.sf-slot-count {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0 0 .75rem;
}
.sf-slot-count span {
  font-size: 1rem;
  font-weight: 400;
  color: var(--muted);
}

/* Three little bars: filled = slot taken.
   NOTE: .sf-slot is a 10px bar and belongs to this module. Attendance class
   cards are .sf-class-card -- do not reuse .sf-slot for them, or every card
   collapses to the height of a device bar and its content spills over the
   card below it. */
.sf-slot-bar { display: flex; gap: .4rem; margin-bottom: .85rem; }
.sf-slot {
  flex: 1;
  height: 10px;
  border-radius: 999px;
  background: var(--border);
}
.sf-slot-full { background: var(--primary); }

/* Destructive full-width action, e.g. reset all other devices. */
.sf-btn-danger {
  background-color: var(--card);
  color: var(--danger-text);
  border: 1px solid var(--danger-line);
}
.sf-btn-danger:hover, .sf-btn-danger:focus {
  background-color: var(--danger-bg);
  color: var(--danger-text);
}

/* ---------- Section placeholders ----------
   Empty-state cards for sections whose module has not been built yet. */
.sf-placeholder {
  border-top: 4px solid var(--accent);
  text-align: center;
  padding: 2rem 1.25rem;
}
.sf-placeholder .sf-h2 { margin-bottom: .5rem; }
.sf-placeholder .sf-btn { margin-top: 1.25rem; }

/* ---------- Membership plans ---------- */
.sf-plan { border-left: 4px solid var(--accent); }

/* Retired plans stay legible but visibly stand down. */
.sf-plan-retired { border-left-color: var(--border); opacity: .72; }

.sf-price {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text);
  margin: .6rem 0 0;
  font-variant-numeric: tabular-nums;
}

.sf-plan-note { font-size: .9rem; margin: .5rem 0 0; }

/* Duration quick-picks: a shortcut, never the only way to enter a value. */
.sf-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .5rem; }

.sf-chip {
  min-height: 40px;
  padding: 0 .9rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font-size: .88rem;
  font-weight: 600;
  cursor: pointer;
}
.sf-chip:hover, .sf-chip:focus { background: var(--surface-2); }
.sf-chip-on { background: var(--primary-strong); border-color: var(--primary-strong); color: var(--primary-text); }

/* Rupee sign sitting inside the price box. */
.sf-prefix {
  position: absolute;
  left: .9rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  font-size: 1rem;
  pointer-events: none;
}
.sf-input-prefixed { padding-left: 2.1rem; }

/* ---------- Permanent delete ----------
   Deliberately louder than Retire/Deactivate: this is the irreversible one. */
.sf-danger { border-top: 4px solid var(--danger); }

.sf-danger-eyebrow {
  text-transform: uppercase;
  letter-spacing: .09em;
  font-size: .72rem;
  font-weight: 700;
  color: var(--danger-text);
  margin: 0 0 .35rem;
}

.sf-consequences {
  margin: 1rem 0 1.25rem;
  padding-left: 1.1rem;
  color: var(--text);
  font-size: .93rem;
  line-height: 1.55;
}
.sf-consequences li { margin-bottom: .4rem; }

/* Echoes the exact text that has to be retyped. */
.sf-echo {
  display: inline-block;
  background: var(--danger-bg);
  color: var(--danger-text);
  border-radius: 5px;
  padding: 1px 6px;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: .9em;
}

.sf-btn-destroy {
  background-color: var(--danger);
  border-color: var(--danger);
  color: var(--header-text);
}
.sf-btn-destroy:hover, .sf-btn-destroy:focus { background-color: var(--danger-text); color: var(--header-text); }

.sf-btn-sm-destroy { color: var(--danger-text); border-color: var(--danger-line); }
.sf-btn-sm-destroy:hover, .sf-btn-sm-destroy:focus { background: var(--danger-bg); color: var(--danger-text); }

/* ---------- Amber call to action ----------
   One per screen at most: the action the studio actually wants tapped, such as
   Renew or Pay. Dark text on amber -- white would fail contrast badly. */
.sf-btn-accent {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}
.sf-btn-accent:hover, .sf-btn-accent:focus {
  background-color: var(--accent-dark);
  color: var(--accent-text);
}

/* Status text for lists that show paid / overdue without a pill. */
.sf-status-ok   { color: var(--success-text); font-weight: 600; }
.sf-status-bad  { color: var(--danger-text);  font-weight: 600; }

/* ---------- Members ---------- */
.sf-search { display: flex; gap: .5rem; margin-bottom: 1rem; }
.sf-search .sf-input { flex: 1; }

/* Whole card is the tap target into the member's record. */
.sf-member { display: block; text-decoration: none; color: inherit; }
.sf-member:hover, .sf-member:focus { border-color: var(--primary); }

/* Expired reads red rather than the neutral grey used for "retired". */
.sf-pill-expired { background: var(--danger-bg); color: var(--danger-text); }

.sf-dues {
  background: var(--danger-bg);
  color: var(--danger-text);
  border-radius: 999px;
  padding: .2rem .6rem;
  font-size: .75rem;
  font-weight: 700;
  white-space: nowrap;
}

.sf-member-head { border-top: 4px solid var(--primary); }

/* Health note stands out without shouting. */
.sf-health { border-left: 4px solid var(--accent); }

.sf-facts { margin: .9rem 0 0; padding: 0; }
.sf-facts > div {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .45rem 0;
  border-top: 1px solid var(--border);
  font-size: .92rem;
}
.sf-facts dt { color: var(--muted); margin: 0; }
.sf-facts dd { margin: 0; font-weight: 600; text-align: right; }

/* Money summary: fee, paid, pending. */
.sf-money-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  padding: .5rem 0;
  font-size: .95rem;
}
.sf-money-row + .sf-money-row { border-top: 1px solid var(--border); }
.sf-money-row strong { font-variant-numeric: tabular-nums; }
.sf-money-total { font-size: 1.1rem; padding-top: .7rem; }
.sf-money-total strong { font-size: 1.35rem; }

.sf-payments { list-style: none; margin: 0; padding: 0; }
.sf-payments li { padding: .65rem 0; border-top: 1px solid var(--border); }
.sf-payments li:first-child { border-top: none; }
.sf-payments strong { font-size: 1.05rem; font-variant-numeric: tabular-nums; }

.sf-method {
  margin-left: .5rem;
  background: var(--surface-2);
  color: var(--muted);
  border-radius: 999px;
  padding: .1rem .55rem;
  font-size: .72rem;
  font-weight: 700;
}
.sf-payment-meta { font-size: .84rem; margin-top: .15rem; }

/* Live expiry preview on the enrolment form. */
.sf-expiry-preview {
  border-left: 4px solid var(--primary);
  padding: .85rem 1rem;
  margin-bottom: 1rem;
}
.sf-expiry {
  margin: .2rem 0 0;
  font-size: 1.2rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.sf-section { margin: 1.5rem 0 .75rem; padding-top: 1.25rem; border-top: 1px solid var(--border); }

.sf-textarea { min-height: 88px; padding-top: .7rem; resize: vertical; line-height: 1.5; }

/* UPI / Cash toggle built from real radios, so it works without JavaScript. */
.sf-chip-radio { position: relative; display: inline-flex; align-items: center; }
.sf-chip-radio input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.sf-chip-radio:has(input:checked) {
  background: var(--primary-strong);
  border-color: var(--primary-strong);
  color: var(--primary-text);
}
.sf-chip-radio:has(input:focus-visible) { outline: 2px solid var(--primary); outline-offset: 2px; }

/* Class-type tag on a plan, batch or member card. Filled rather than outlined,
   so the activity is the first thing read on the card. Small but legible: the
   uppercase and the letter-spacing carry it at .68rem. */
.sf-activity {
  /* inline-flex, and a minimum height taken from the size the caller asked
     for. Both matter for consistency: the three activities sit side by side
     all over the app, and only one of them has artwork so far. Without this
     the one with an icon stood taller than the two without. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--sf-act-size, 1rem) + .24rem);
  background: var(--activity-aerobics);
  color: var(--activity-aerobics-text);
  border-radius: 999px;
  padding: .12rem .5rem;
  margin-right: .35rem;
  font-size: .68rem;
  font-weight: 800;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- batches */

/* The fill per class type. A new activity with no rule here simply falls back
   to the teal defined on .sf-activity, so nothing ever renders unstyled. */
.sf-activity-aerobics { background: var(--activity-aerobics); color: var(--activity-aerobics-text); }
.sf-activity-yoga     { background: var(--activity-yoga);     color: var(--activity-yoga-text); }
.sf-activity-dance    { background: var(--activity-dance);    color: var(--activity-dance-text); }

/* Session divider: Morning, then Evening. Sits between cards, not on one. */
.sf-session-head {
  margin: 1.25rem 0 .5rem;
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}
.sf-session-head:first-of-type { margin-top: .25rem; }

/* A batch card leads with its time, so give that line a teal edge to scan down. */
.sf-batch { border-left: 4px solid var(--primary); }
.sf-batch-stopped { opacity: .62; border-left-color: var(--border); }

/* Who teaches it. An assigned instructor reads as a fact; an unassigned batch
   should look unfinished, because it is. */
.sf-instructor {
  font-weight: 600;
  color: var(--text);
}
.sf-instructor.sf-unassigned {
  font-weight: 500;
  font-style: italic;
  color: var(--danger-text);
}

/* Start and end time side by side -- they are one thought and fit a phone row. */
.sf-field-row {
  display: flex;
  gap: .75rem;
}
.sf-field-row > .sf-field { flex: 1 1 0; min-width: 0; }

/* ------------------------------------------------- member home batch */

/* The batch card on a member's details screen, matching the batch cards on the
   timetable so the same thing looks the same in both places. */
.sf-batch-card { border-left: 4px solid var(--primary); }

/* The "12 of 20 members" note under the batch dropdown. It is advice, not a
   limit, so it stays quiet until the class is actually full. */
.sf-batch-note { margin-top: .35rem; }
.sf-status-warn {
  color: var(--danger-text);
  font-weight: 600;
}

/* ------------------------------------------------- plans: filter + grouping */

/* Activity filter chips. They are links, so the choice survives a reload and
   works from the PWA cache. Scrolls sideways only if a fourth activity is ever
   added -- with three it fits a phone without scrolling at all. */
.sf-filter-row {
  display: flex;
  gap: .4rem;
  margin-bottom: .9rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.sf-filter-row::-webkit-scrollbar { display: none; }

/* Tall enough to tap comfortably without eating the screen: the chips sit just
   under the header, above the Add button. */
.sf-filter {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  min-height: 44px;
  padding: 0 .8rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font-size: .82rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  flex: 0 0 auto;
}
.sf-filter:active { background: var(--surface-2); }

/* The count rides along so Grace can see there are no Dance plans yet without
   having to tap through to an empty list. */
.sf-filter-count {
  font-size: .72rem;
  font-weight: 700;
  color: var(--muted);
}

/* The selected chip takes its activity's own colour, matching the badges on
   the cards below it. */
.sf-filter-on {
  background: var(--primary-strong);
  border-color: var(--primary-strong);
  color: var(--primary-text);
}
.sf-filter-on .sf-filter-count { color: var(--primary-text); opacity: .8; }

.sf-filter-aerobics.sf-filter-on {
  background: var(--activity-aerobics);
  border-color: var(--activity-aerobics);
  color: var(--activity-aerobics-text);
}
.sf-filter-yoga.sf-filter-on {
  background: var(--activity-yoga);
  border-color: var(--activity-yoga);
  color: var(--activity-yoga-text);
}
.sf-filter-dance.sf-filter-on {
  background: var(--activity-dance);
  border-color: var(--activity-dance);
  color: var(--activity-dance-text);
}
.sf-filter-aerobics.sf-filter-on .sf-filter-count,
.sf-filter-yoga.sf-filter-on .sf-filter-count { color: var(--activity-aerobics-text); opacity: .75; }
.sf-filter-dance.sf-filter-on .sf-filter-count { color: var(--activity-dance-text); opacity: .8; }

/* Group heading on the unfiltered list. A short colour bar rather than a full
   band, so it separates the groups without shouting over the cards. */
.sf-group-head {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 1.35rem 0 .55rem;
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}
.sf-group-head::before {
  content: "";
  width: 3px;
  height: 1rem;
  border-radius: 2px;
  background: var(--activity-aerobics);
}
.sf-group-head:first-of-type { margin-top: .25rem; }

.sf-group-aerobics::before { background: var(--activity-aerobics); }
.sf-group-yoga::before     { background: var(--activity-yoga); }
.sf-group-dance::before    { background: var(--activity-dance); }

/* Badge above the plan name, so the eye hits the activity first and the name
   is not pushed into a second line on a narrow phone. */
.sf-plan-title { min-width: 0; }
.sf-plan-title .sf-activity { margin-bottom: .25rem; }

/* ------------------------------------------------ membership freeze / pause */

/* The third state on the status pill, alongside sf-pill-on and sf-pill-off. */
.sf-pill-frozen {
  background: var(--frozen);
  color: var(--frozen-text);
}

/* "Frozen since 3 Sep 2026" on a card. Quieter than the pill -- the pill
   already says the state, this only adds the date. */
.sf-frozen-since {
  color: var(--frozen);
  font-weight: 600;
  font-size: .82rem;
}

/* The panel on the details screen while a membership is paused. The tinted
   background sets it apart from the ordinary white cards around it. */
.sf-frozen-card {
  background: var(--frozen-bg);
  border-left: 4px solid var(--frozen);
}

/* A plain explanatory card, used above the freeze and resume forms to say what
   will and will not move. */
.sf-note {
  background: var(--surface-2);
  border-left: 4px solid var(--border);
}

/* Freeze history, laid out like the payment list so the two read alike. */
.sf-freezes { list-style: none; margin: 0; padding: 0; }
.sf-freezes li {
  padding: .6rem 0;
  border-top: 1px solid var(--border);
}
.sf-freezes li:first-child { border-top: 0; }

/* The freeze that is still running gets the same slate marker as the pill. */
.sf-freeze-open {
  border-left: 3px solid var(--frozen);
  padding-left: .55rem;
}

/* The state chips reuse the plans filter row. Frozen takes the slate; Active
   and Expired borrow the pill colours they match on the cards below. */
.sf-filter-active.sf-filter-on {
  background: var(--success);
  border-color: var(--success);
  color: #FFFFFF;
}
.sf-filter-frozen.sf-filter-on {
  background: var(--frozen);
  border-color: var(--frozen);
  color: var(--frozen-text);
}
.sf-filter-expired.sf-filter-on {
  background: var(--danger-text);
  border-color: var(--danger-text);
  color: #FFFFFF;
}
.sf-filter-active.sf-filter-on .sf-filter-count,
.sf-filter-frozen.sf-filter-on .sf-filter-count,
.sf-filter-expired.sf-filter-on .sf-filter-count { color: #FFFFFF; opacity: .8; }

/* --------------------------------------------- member profile & weight log */

/* The live age readout under the date-of-birth field. Teal rather than muted,
   because it is a computed answer rather than instructions. */
.sf-age-note {
  color: var(--primary-strong);
  font-weight: 700;
  margin-top: .35rem;
}

/* Emergency contact stands slightly apart from the ordinary cards: it is the
   one thing on the screen someone might need in a hurry. */
.sf-emergency { border-left: 4px solid var(--danger); }
.sf-emergency-call {
  display: inline-block;
  margin-top: .3rem;
  min-height: 44px;
  line-height: 44px;
  color: var(--danger-text);
  font-weight: 800;
  font-size: 1.05rem;
  text-decoration: none;
}

/* Weight summary: starting figure, arrow, current figure. Wraps rather than
   scrolls if the numbers get long on a narrow phone. */
.sf-weight-summary {
  display: flex;
  align-items: center;
  gap: .9rem;
  flex-wrap: wrap;
  margin: .6rem 0 .35rem;
}
.sf-weight-figure {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 800;
  color: var(--text);
}
.sf-weight-arrow {
  font-size: 1.2rem;
  color: var(--muted);
}

/* The change line. Its colour comes from the view model -- green for a loss,
   red for a gain, muted when it has not moved. */
.sf-weight-change {
  margin: 0 0 .5rem;
  font-size: 1.05rem;
  font-weight: 800;
}

/* BMI sits in its own tinted strip so the disclaimer travels with the number
   and cannot be read apart from it. */
.sf-bmi {
  background: var(--surface-2);
  border-radius: var(--radius);
  padding: .6rem .7rem;
  margin-bottom: .75rem;
}

/* Weigh-in history, laid out like the payments and freeze lists. */
.sf-weights { list-style: none; margin: 0; padding: 0; }
.sf-weights li {
  padding: .6rem 0;
  border-top: 1px solid var(--border);
}
.sf-weights li:first-child { border-top: 0; }

/* Reading on the left, the admin-only delete on the right. */
.sf-weight-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .6rem;
}
.sf-weight-row > div { min-width: 0; }

/* ---------------------------------------------- waive-off & revenue report */

/* The fee discount line in the money block: agreed once at enrolment. */
.sf-money-off { color: var(--muted); }

/* A settlement waive-off. Amber rather than green, because the dues closed but
   no money came in -- it must never read like income. */
.sf-money-waived { color: var(--accent-dark); }
.sf-waiver-reason {
  margin: -.15rem 0 .35rem;
  text-align: right;
}

/* The secondary line on a payment row. Indented and amber so it is plainly a
   note about the payment rather than part of the amount received. */
.sf-waiver-line {
  margin-top: .2rem;
  padding-left: .55rem;
  border-left: 3px solid var(--accent);
  color: var(--accent-dark);
  font-size: .8rem;
  font-weight: 700;
}

/* The collapsed waive-off section on the payment form. Shut by default: most
   payments are ordinary, and writing money off should be a deliberate act. */
.sf-waive {
  background: var(--warn-bg);
  border-radius: var(--radius);
  padding: .1rem .7rem .5rem;
  margin-bottom: .9rem;
}
.sf-waive-summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-weight: 800;
  color: var(--accent-dark);
  cursor: pointer;
  list-style: none;
}
.sf-waive-summary::-webkit-details-marker { display: none; }
.sf-waive-summary::before {
  content: "+";
  margin-right: .45rem;
  font-size: 1.1rem;
  line-height: 1;
}
.sf-waive[open] .sf-waive-summary::before { content: "\2212"; }

/* The live settlement summary. Reads as a small receipt, so what is being
   agreed is unambiguous before it is saved. */
.sf-settle { background: var(--surface-2); }
.sf-settle-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: .3rem 0;
  font-size: .92rem;
}
.sf-settle-waived strong { color: var(--accent-dark); }
.sf-settle-total {
  border-top: 1px solid var(--border);
  margin-top: .25rem;
  padding-top: .5rem;
  font-size: 1rem;
  font-weight: 800;
}

/* Two figures on the revenue report, in two cards, never added together. */
.sf-revenue { border-left: 4px solid var(--border); }
.sf-revenue-figure {
  margin: .1rem 0 .3rem;
  font-size: 1.7rem;
  font-weight: 800;
  color: var(--text);
}
.sf-revenue-collected { border-left-color: var(--success); }
.sf-revenue-waived { border-left-color: var(--accent); }
.sf-revenue-waived .sf-revenue-figure { color: var(--accent-dark); }

.sf-mt-2 { margin-top: .5rem; }

/* -------------------------------------------------------- membership renewal */

/* The renewal prompt on the details screen. Amber while it is coming up, red
   once the membership has actually lapsed. */
.sf-renew-card {
  background: var(--warn-bg);
  border-left: 4px solid var(--accent);
}
.sf-renew-urgent {
  background: var(--danger-bg);
  border-left-color: var(--danger);
}

/* Dues carried over from a closed period. Deliberately its own card, so old
   money is never mistaken for part of the current period's figures. */
.sf-dues-warning {
  background: var(--danger-bg);
  border-left: 4px solid var(--danger);
}
.sf-dues-figure {
  margin: .1rem 0 .3rem;
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--danger-text);
}

/* The countdown on a member row and the quick Renew beside it. */
.sf-countdown {
  color: var(--accent-dark);
  font-weight: 700;
  font-size: .82rem;
}
.sf-countdown-late { color: var(--danger-text); }

.sf-quick-renew {
  display: inline-block;
  margin: -.55rem 0 .9rem;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 800;
}

/* Membership history: one block per period, laid out like the other lists. */
.sf-periods { list-style: none; margin: 0; padding: 0; }
.sf-periods li {
  padding: .7rem 0;
  border-top: 1px solid var(--border);
}
.sf-periods li:first-child { border-top: 0; }

/* The period the member is in right now gets a teal edge. */
.sf-period-current {
  border-left: 3px solid var(--primary);
  padding-left: .55rem;
}

.sf-period-money {
  display: flex;
  flex-wrap: wrap;
  gap: .1rem .9rem;
  margin-top: .35rem;
  font-size: .84rem;
  font-weight: 600;
}

/* The untracked days between two periods, and the carried-over dues line, both
   on the renewal summary. */
.sf-settle-gap strong { color: var(--muted); font-weight: 600; }
.sf-settle-prev {
  border-top: 1px solid var(--border);
  margin-top: .25rem;
  padding-top: .5rem;
}
.sf-settle-prev strong { color: var(--danger-text); }

/* ------------------------------------------------------------- attendance */

/* Day stepper: back a day, the picker, forward a day. Arrows are the common
   case, so they get their own tap targets either side. */
.sf-daybar {
  display: flex;
  align-items: center;
  gap: .5rem;
}
.sf-daybar-form { flex: 1 1 auto; display: flex; gap: .4rem; }
.sf-daybar-form .sf-input { margin: 0; }

/* One class on the day view. Named .sf-class-card, NOT .sf-slot: that name is
   already a 10px device bar in the Manage Devices module, and reusing it
   collapsed every card to 42px. The left edge carries the state so a column of
   classes can be read at a glance without reading any words. */
.sf-class-card { border-left: 4px solid var(--border); }
.sf-class-pending   { border-left-color: var(--border); }
.sf-class-open      { border-left-color: var(--success); background: var(--success-bg); }
.sf-class-marked    { border-left-color: var(--primary); }
.sf-class-locked    { border-left-color: var(--muted); opacity: .72; }
.sf-class-cancelled { border-left-color: var(--danger); opacity: .72; }

/* Time on the left, state chip on the right, both able to shrink. min-width:0
   is what lets the title truncate instead of shoving the chip off-screen. */
.sf-class-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: .6rem;
  flex-wrap: wrap;
}
.sf-class-title { min-width: 0; flex: 1 1 60%; }
.sf-class-time {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--text);
}
.sf-class-who {
  margin: .15rem 0 0;
  font-size: .85rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chip never shrinks below its text, and wraps to its own line rather than
   being squeezed off the card. */
.sf-state-chip {
  flex: 0 0 auto;
  display: inline-block;
  max-width: 100%;
  padding: .2rem .6rem;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sf-state-open      { background: var(--success); color: #FFFFFF; }
.sf-state-marked    { background: var(--primary-strong); color: var(--primary-text); }
.sf-state-locked    { background: var(--surface-2); color: var(--muted); }
.sf-state-cancelled { background: var(--danger-bg); color: var(--danger-text); }

/* The cancellation reason can be long, so it wraps under the card rather than
   living inside the chip. */
.sf-class-note {
  margin: .5rem 0 0;
  font-size: .82rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}
.sf-class-cancelled .sf-class-note { color: var(--danger-text); }

/* The action on an open slot is the one the marker wants; make it findable. */
.sf-btn-sm-go {
  background: var(--success);
  border-color: var(--success);
  color: #FFFFFF;
  font-weight: 800;
}

/* A slot the viewer may not open. Visible but plainly inert. */
.sf-btn-sm-disabled {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 0 .7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: .82rem;
  font-weight: 700;
  cursor: not-allowed;
}

/* Somebody taught a class that was not theirs. */
.sf-cover-flag {
  display: inline-block;
  margin-left: .3rem;
  padding: .05rem .4rem;
  border-radius: 999px;
  background: var(--warn-bg);
  color: var(--accent-dark);
  font-size: .68rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: .04em;
}

/* The register itself. One row per member, name on the left, one big toggle on
   the right -- sized for a thumb while standing in a studio. */
.sf-register { list-style: none; margin: 0; padding: 0; }
.sf-register-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .7rem;
  min-height: 48px;              /* comfortable row, tapped 25 times in a row */
  padding: .55rem 0;
  border-top: 1px solid var(--border);
}
.sf-register-row:first-child { border-top: 0; }

/* The name takes whatever width is left and truncates; the toggle beside it
   keeps its own size. Without min-width:0 the name would refuse to shrink and
   push the toggle off the edge of the card. */
.sf-register-who { min-width: 0; flex: 1 1 auto; }
.sf-register-name {
  display: block;
  color: var(--text);
  font-weight: 700;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sf-register-flags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem .4rem;
  margin-top: .15rem;
  font-size: .78rem;
}

.sf-pill-visitor {
  background: var(--accent);
  color: var(--accent-text);
}

/* ---- the register's Renew shortcut ----

   A membership about to lapse, or just lapsed, sorted out without leaving the
   class. It sits in the wrapping flags row inside the name column, beside the
   Expired badge, so the Present/Absent toggle keeps its own width and the row
   still fits 360px with the name truncating first.

   BADGE-SIZED, NOT BUTTON-SIZED. It is deliberately smaller than the 44px the
   app gives a primary action: this one shares a line with a status pill on the
   busiest screen in the studio, and a full-height button beside every lapsing
   member would read as the row's main action instead of the toggle. 28px is
   still a real target at this size, and the row it sits in is 48px of dead
   space around it -- a near miss lands on nothing rather than on the toggle,
   which is the failure worth avoiding.

   Outlined rather than filled, for the same reason: the toggle is the one
   filled control on the row and has to stay the loudest thing on it. */
.sf-register-renew {
  display: inline-flex;
  align-items: center;
  flex: none;
  min-height: 28px;
  padding: 0 .55rem;
  border: 1.5px solid var(--primary-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--primary-strong);
  font-size: .72rem;
  font-weight: 800;
  letter-spacing: .02em;
  text-decoration: none;
  white-space: nowrap;
}
.sf-register-renew:hover,
.sf-register-renew:focus-visible {
  background: var(--surface-2);
  color: var(--primary-dark);
}

/* The line that says unsaved marks were put back after a trip to Renew. Quiet:
   it is a reassurance, not a warning, and it is gone on the next load. */
.sf-register-restored {
  margin: -.2rem 0 .6rem;
  font-size: .8rem;
  font-weight: 700;
  color: var(--primary-strong);
}

/* Where a visitor normally goes. Sits beside the Visitor badge in the same
   wrapping flags row, so a long one drops to its own line inside the name
   column and never squeezes the Present/Absent toggle, which keeps its own
   width. Truncated as well, for the case where it wraps to a line of its own
   and the batch time is somehow longer than the card. */
.sf-visitor-from {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-weight: 700;
  font-size: .76rem;
}

/* A member already down in another class today. A note, never a blocker. */
.sf-clash-note {
  color: var(--accent-dark);
  font-weight: 700;
  font-size: .76rem;
}

/* The toggle. Wide and tall on purpose: it is tapped 25 times in a row. */
.sf-attend {
  flex: 0 0 auto;
  min-width: 96px;
  min-height: var(--tap);
  border: 2px solid transparent;
  border-radius: var(--radius);
  font-size: .88rem;
  font-weight: 800;
  cursor: pointer;
}
.sf-attend-present {
  background: var(--success);
  border-color: var(--success);
  color: #FFFFFF;
}
.sf-attend-absent {
  background: var(--card);
  border-color: var(--border);
  color: var(--muted);
}
.sf-attend:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* An admin writing up a register after its window shut. */
.sf-late-note { border-left-color: var(--accent); }

/* --------------------------------------------- shared mobile layout rules */

/* One line, cut with an ellipsis rather than pushing its neighbours out of the
   row. Needs min-width:0 on the flex parent to have any effect. */
.sf-truncate {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* An action bar pinned to the foot of the phone frame. On a main page it rides
   above the tab bar rather than on top of it, and on an inner page it clears
   the gesture bar. */
/* FIXED, NOT STICKY, AND THAT IS THE FIX RATHER THAN A PREFERENCE.
   This bar was position: sticky; bottom: 0 and it never once stuck. Both
   .sf-app and .sf-frame carry overflow-x: hidden as a guard against a stray
   wide child, and overflow-x: hidden computes overflow-y to auto -- which makes
   .sf-frame the scrollport for every sticky descendant. The frame grows with
   its content and never scrolls itself, so a sticky child had nothing to stick
   against and simply laid out in flow, at the very bottom of a 25-name roster:
   measured at top 1730px in a 602px viewport. Exactly where it was not wanted.

   position: fixed is how .sf-appbar and .sf-tabbar already solve this same
   problem in this same frame, so the bar is centred and capped to --frame-w the
   way they are -- fixed is measured from the viewport, so without that it would
   span a laptop window instead of the phone column.

   z-index 15 sits under the tab bar's 20 and over the content, so the tab bar
   stays on top wherever both are drawn. */
.sf-sticky-save {
  position: fixed;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 100%;
  max-width: var(--frame-w);

  z-index: 15;

  /* Tight on purpose. The bar is permanently on screen, so its padding is
     register nobody can see: .45rem keeps it legible without eating a row. */
  padding: .45rem 1rem calc(.45rem + env(safe-area-inset-bottom, 0px));

  /* Solid, not a fade: a half-transparent bar lets the row underneath show
     through the button, which reads as broken rather than layered. */
  background: var(--bg);
  border-top: 1px solid var(--border);
}
.sf-sticky-save .sf-btn { margin: 0; }

/* ABOVE THE TAB BAR, ON THE PAGES THAT HAVE ONE.
   Both are fixed to the same viewport edge, so the save bar has to be lifted by
   the tab bar's height or it would sit underneath it. The register has no tab
   bar today -- it is not a main page -- but the offset is written anyway so the
   bar is correct wherever it is used, and so making the register a tabbed
   screen later cannot quietly hide its Save button. The safe-area inset moves
   from the padding to the offset here, because the tab bar already clears it. */
.sf-app:not(.sf-app-notabs) .sf-sticky-save {
  bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: .45rem;
}

/* A fixed bar is out of flow, so the last names in the roster would sit behind
   it. The form reserves the room instead. Generous on purpose: the bar is one
   row on most phones and two when it wraps, and this covers the taller case --
   it costs a little scrolling past the end of the list and nothing at all on
   screen. */
[data-sf-register] { padding-bottom: 8rem; }

/* ---- the register's save bar ----

   ONE COMPACT ROW: the count, the two shortcuts, and Save. It repeats what the
   top of the card already shows, because a 25-name roster is saved from down
   here and the top has long since scrolled away.

   It wraps rather than squeezing. At 360px the four controls come to about the
   width of the column, so Save drops to a second line if it must -- which is
   the right thing to give way: it is full-width and unmissable either way,
   whereas a squeezed count or a clipped chip is just broken.

   THE REGISTER STAYS THE MAIN CONTENT. Measured at 360, 390 and 414px the bar
   wraps to two lines and comes to 118px, against a 70px register row, which
   leaves six names on screen under a 56px app bar. Six is the floor this was
   built to, and it is why the padding, the chips and Save are all trimmed
   below the app's standard sizes rather than left at them. */
.sf-savebar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem .5rem;
}

/* The same figure as the card header, in the same shape, so the two read as
   one number in two places rather than as two facts. */
.sf-savebar-count {
  flex: none;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  padding: 0 .6rem;
  border-radius: 999px;
  background: var(--surface-2);
  font-size: .95rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.sf-savebar-alls { display: inline-flex; gap: .35rem; flex: none; }

/* Save takes whatever is left, and never less than a thumb's worth. When the
   row wraps it is alone on its line and stretches the full width.

   44px rather than the standard button height: this bar is permanently on
   screen, so every pixel it takes is a pixel of register nobody can see. 44 is
   still a full tap target -- it is the floor, not a squeeze. */
.sf-savebar-go { flex: 1 1 9rem; min-width: 9rem; min-height: 44px; }

/* The shortcuts in the save bar are the compact set: the top of the card has
   room for "Mark all Present", a bar sharing a line with a count and a Save
   button does not. Still a 36px target -- small, never untappable. */
.sf-chip-sm {
  min-height: 36px;
  padding: 0 .6rem;
  font-size: .8rem;
}

/* "+ Add member" reads as an action rather than a filter, so it is tinted like
   one. It is a link, not a button: it leaves the screen. */
.sf-chip-add {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  border-color: var(--primary-strong);
  color: var(--primary-strong);
  font-weight: 700;
}
.sf-chip-add:hover, .sf-chip-add:focus { background: var(--surface-2); color: var(--primary-dark); }

/* A sub-header that stays put while the list under it scrolls. Offset by the
   app bar so it can never slide underneath it. */
.sf-subhead {
  position: sticky;
  top: var(--appbar-h);
  z-index: 4;
  margin: 0 -1rem 1rem;
  padding: .75rem 1rem;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

/* Chips are tapped, so they get a real target rather than 40px. */
.sf-chip { min-height: 44px; }

/* Long words -- a pasted note, an email -- break instead of widening the page. */
.sf-card, .sf-register-row, .sf-slot-state, .sf-hint { overflow-wrap: anywhere; }

/* ------------------------------------------------------ payments: fast scan */

/* A–Z strip. The only thing on the page allowed to scroll sideways, and it
   hides its scrollbar so it reads as a strip rather than a widget. */
.sf-az {
  display: flex;
  gap: .25rem;
  margin-bottom: 1rem;
  padding-bottom: .2rem;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.sf-az::-webkit-scrollbar { display: none; }

.sf-az-key {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  padding: 0 .5rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card);
  color: var(--text);
  font-size: .9rem;
  font-weight: 700;
  text-decoration: none;
}
.sf-az-key:active { background: var(--surface-2); }

/* Dimmed and inert: nothing to open, so nothing to tap. */
.sf-az-empty {
  color: var(--border);
  background: var(--bg);
  border-style: dashed;
  cursor: default;
}
.sf-az-on {
  background: var(--primary-strong);
  border-color: var(--primary-strong);
  color: var(--primary-text);
}

/* Collapsed group headers. The whole header is the control. */
.sf-groups { list-style: none; margin: 0; padding: 0; }
.sf-group { border-top: 1px solid var(--border); }
.sf-group:first-child { border-top: 0; }

.sf-group-head {
  display: flex;
  align-items: center;
  gap: .7rem;
  min-height: 52px;
  padding: .5rem 0;
  color: var(--text);
  text-decoration: none;
}
.sf-group-head:active { background: var(--surface-2); }

.sf-group-letter {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: var(--surface-2);
  color: var(--primary-strong);
  font-weight: 800;
  font-size: .95rem;
}
.sf-group-open .sf-group-letter {
  background: var(--primary-strong);
  color: var(--primary-text);
}

.sf-group-summary {
  flex: 1 1 auto;
  font-size: .88rem;
  color: var(--muted);
}
.sf-group-chevron { flex: 0 0 auto; color: var(--muted); }
.sf-group-body { padding: 0 0 .6rem; }

/* ---- the single-line payment row ---- */
.sf-paylist { list-style: none; margin: 0; padding: 0; }
.sf-paylist > li { border-top: 1px solid var(--rule, var(--border)); }
.sf-paylist > li:first-child { border-top: 0; }

/* Name left, figures right, on ONE line. min-width:0 on the name is what lets
   it truncate instead of shoving the amount off the row. */
.sf-payrow {
  display: flex;
  align-items: center;
  gap: .6rem;
  min-height: 44px;
  padding: .35rem 0;
  color: var(--text);
  text-decoration: none;
}
.sf-payrow:active { background: var(--surface-2); }

.sf-payrow-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .95rem;
  font-weight: 600;
}

/* Never shrinks, never wraps -- the figures are the point of the row. */
.sf-payrow-right {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
}
.sf-payrow-amount {
  font-size: .95rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.sf-payrow-date {
  font-size: .8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.sf-method-chip {
  padding: .1rem .4rem;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--muted);
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .03em;
  text-transform: uppercase;
}

/* A write-off is not part of what was received, so it never joins the figure. */
.sf-payrow-waived {
  margin: -.2rem 0 .4rem;
  padding-left: .1rem;
  color: var(--accent-dark);
  font-size: .76rem;
  font-weight: 700;
}

/* Who recorded a payment. One step quieter than the write-off line above it --
   the write-off changes what the figure means, this only says who typed it. */
.sf-payrow-by {
  margin: -.2rem 0 .4rem;
  padding-left: .1rem;
  color: var(--muted);
  font-size: .72rem;
}

.sf-paylist-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin-top: .7rem;
  padding-top: .6rem;
  border-top: 1px solid var(--border);
  font-size: .82rem;
}

/* At the narrowest phones the date drops out; the amount and method carry the
   row, and the full date is still in the row's title and on the member screen. */
@media (max-width: 359px) {
  .sf-payrow-date { display: none; }
}

/* ------------------------------------------------------- sign-in brand mark */

/* Self-hosted rather than pulled from a font CDN: this is an installed PWA, and
   on studio wifi that drops, an external font silently falls back to a plain
   face. Shipping the file means the signature always looks like the signature. */
@font-face {
  font-family: "Kaushan Script";
  src: url("/fonts/kaushan-script.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* The owner's signature, sitting above the studio name like ink on a shopfront.
   Tilted a couple of degrees so it reads as handwriting rather than a heading. */
.sf-brand-script {
  font-family: "Kaushan Script", "Segoe Script", "Brush Script MT", cursive;
  font-size: clamp(2rem, 11vw, 2.6rem);
  line-height: 1;
  color: var(--brand-pink);
  margin: 0 0 .15rem;
  transform: rotate(-3deg);
  text-shadow: 0 1px 0 rgba(255, 255, 255, .7);
}

/* The studio name, one word per colour. Flex-wrap rather than a plain line, so
   on a narrow phone it breaks between words instead of overflowing. */
.sf-brand-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  gap: .1rem .4rem;
  margin: 0;
  font-size: clamp(1.05rem, 5vw, 1.35rem);
  font-weight: 800;
  letter-spacing: .01em;
  line-height: 1.15;
}

/* Each word carries an activity's colour -- the same ones the badges use
   everywhere else, so the sign-in previews the app. */
.sf-brand-grace  { color: var(--brand-red); }
.sf-brand-aero   { color: var(--activity-aerobics-ink, #0F766E); }
.sf-brand-amp    { color: var(--muted); font-weight: 600; }
.sf-brand-fit    { color: #B45309; }
.sf-brand-centre { color: #6D28D9; }

/* A confetti of the studio's colours under the name -- the one flourish on the
   screen, and it doubles as a divider above the card. */
.sf-auth-head::after {
  content: "";
  display: block;
  width: 132px;
  height: 4px;
  margin: .7rem auto 0;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--brand-red) 0 25%,
    #0F766E 25% 50%,
    #B45309 50% 75%,
    #6D28D9 75% 100%);
}

/* The sign-in button takes the studio red rather than the app teal: it is the
   one moment the brand speaks. White on this red measures 5.94:1. */
.sf-btn-train {
  background-color: var(--brand-red);
  color: #FFFFFF;
  letter-spacing: .01em;
}
.sf-btn-train:active { background-color: #A50E27; }

/* Give the sign-in ground a little warmth so the red sits on it rather than
   floating on flat grey. Kept very pale -- the card must stay the bright thing. */
.sf-body-auth .sf-frame {
  background-image:
    radial-gradient(90% 60% at 50% 0%, rgba(196, 18, 48, .07), transparent 70%),
    radial-gradient(70% 50% at 50% 100%, rgba(109, 40, 217, .05), transparent 70%);
  background-repeat: no-repeat;
}

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

/* A card header that can carry actions on the right without the title being
   squeezed. Wraps rather than overlapping on a narrow phone. */
.sf-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  flex-wrap: wrap;
  margin-bottom: .85rem;
}
.sf-card-head > * { min-width: 0; }
.sf-card-head-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 0 0 auto;
}

/* An action reduced to its glyph. Full 44px target, and the accessible name
   lives on the element so it is never a mystery button to a screen reader. */
.sf-icon-action {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 12px;
  background: var(--primary-strong);
  color: var(--primary-text);
  text-decoration: none;
}
.sf-icon-action svg { width: 21px; height: 21px; }
.sf-icon-action:active { background: var(--primary-dark); }
.sf-icon-action:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* Two tiles to a row on a phone, more if there is room. auto-fit means the
   optional fifth tile flows rather than needing its own rule. */
.sf-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  gap: .55rem;
}

/* The number is what is being scanned for, so it gets the size; the label sits
   under it in small caps. The colour says what needs attention before either
   is actually read. */
.sf-stat {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .6rem .7rem .55rem;
  border-radius: 12px;
  background: var(--surface-2);
  border-left: 3px solid var(--border);
}
.sf-stat-value {
  font-size: 1.6rem;
  font-weight: 800;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
.sf-stat-label {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
}

/* One tint per meaning, reusing the palette the rest of the app already uses
   for the same ideas: green done, amber waiting, red called off. */
.sf-stat-neutral { background: var(--surface-2);  border-left-color: var(--primary-strong); }
.sf-stat-neutral .sf-stat-value { color: var(--primary-strong); }

.sf-stat-good    { background: var(--success-bg); border-left-color: var(--success); }
.sf-stat-good .sf-stat-value    { color: var(--success-text); }

.sf-stat-warn    { background: var(--warn-bg);    border-left-color: var(--accent); }
/* The amber text is the deep --warn-text, not --accent-dark: #D97706 on the
   warn tint measures only 3.07:1, which scrapes past the large-text minimum and
   fails everywhere else. #92400E on the same tint measures 7.44:1, and still
   reads plainly as amber. */
.sf-stat-warn .sf-stat-value    { color: var(--warn-text); }

.sf-stat-bad     { background: var(--danger-bg);  border-left-color: var(--danger); }
.sf-stat-bad .sf-stat-value     { color: var(--danger-text); }

.sf-stat-people  { background: var(--violet-bg);  border-left-color: #7C3AED; }
.sf-stat-people .sf-stat-value  { color: var(--violet-text); }

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

/* Two boxes side by side, and they stay two even on the narrowest phone --
   money in on the left, money owed on the right, comparable at a glance. */
.sf-money-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .6rem;
  margin-bottom: 1rem;
}

/* The whole tile is the link. At this width a separate button would eat most of
   the room the figure needs, so the affordance is the row at the bottom. */
.sf-money-tile {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-width: 0;
  padding: .7rem .75rem .6rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  border-left-width: 3px;
  background: var(--card);
  color: var(--text);
  text-decoration: none;
}
.sf-money-tile:active { filter: brightness(.97); }
.sf-money-tile:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

.sf-money-tile-label {
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Tabular figures so the two tiles line up, and a size that still fits
   a six-figure amount inside half a 360px screen. */
.sf-money-tile-value {
  font-size: 1.1rem;
  font-weight: 800;
  line-height: 1.25;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.sf-money-tile-sub {
  font-size: .66rem;
  line-height: 1.4;
  color: var(--muted);
}

/* Waived money is never added to what was collected, so it reads as its own
   amber figure rather than as part of the number above it. */
.sf-money-tile-waived { color: var(--accent-dark); font-weight: 700; }

.sf-money-tile-go {
  margin-top: .35rem;
  font-size: .64rem;
  font-weight: 700;
  color: var(--primary-strong);
}

.sf-money-in  { border-left-color: var(--success); background: var(--success-bg); }
.sf-money-in .sf-money-tile-value { color: var(--success-text); }

.sf-money-due { border-left-color: var(--danger); background: var(--danger-bg); }
.sf-money-due .sf-money-tile-value { color: var(--danger-text); }
.sf-money-due .sf-money-tile-go { color: var(--danger-text); }

/* ----------------------------------------------------------- quick actions */

/* One flowing row that wraps, instead of a column of full-width buttons. */
.sf-quick {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
}

/* Small type, full-size target: the label shrinks, the tap area does not. */
.sf-quick-btn {
  flex: 0 1 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 .8rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: .74rem;
  font-weight: 700;
  letter-spacing: .01em;
  text-decoration: none;
  white-space: nowrap;
}
.sf-quick-btn:active { filter: brightness(.95); }
.sf-quick-btn:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* Adding a member is the one thing done every day, so it is the only solid
   fill -- the rest are tinted, which keeps six chips from shouting at once. */
.sf-quick-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
}

.sf-quick-teal   { background: var(--surface-2); border-color: var(--primary-strong); color: var(--primary-strong); }
.sf-quick-violet { background: var(--violet-bg);  border-color: #7C3AED;              color: var(--violet-text); }
.sf-quick-green  { background: var(--success-bg); border-color: var(--success);        color: var(--success-text); }
.sf-quick-navy   { background: var(--surface-2);  border-color: var(--header);         color: var(--header); }
.sf-quick-amber  { background: var(--warn-bg);    border-color: var(--accent);         color: var(--accent-dark); }
.sf-quick-plain  { background: var(--card);       border-color: var(--border);         color: var(--muted); }

/* --------------------------------------------------- tappable stat tiles */

/* The Members figures are not just numbers to read -- each one is a list the
   owner wants to open. Making the whole tile the link means no row of buttons
   underneath repeating what the tiles already say. */
a.sf-stat {
  text-decoration: none;
  color: inherit;
  min-height: 66px;
  justify-content: center;
  transition: transform .08s ease;
}
a.sf-stat:active { transform: scale(.98); filter: brightness(.97); }
a.sf-stat:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* A tiny chevron in the corner, so a tile reads as somewhere to go rather than
   just somewhere to look. */
.sf-stat-go {
  position: absolute;
  top: .5rem;
  right: .55rem;
  font-size: .8rem;
  line-height: 1;
  opacity: .45;
}
a.sf-stat { position: relative; }

/* Frozen sits outside the green/amber/red run on purpose: a paused membership
   is neither running nor finished. Slate on the frozen tint measures 7.4:1. */
.sf-stat-frozen { background: var(--frozen-bg); border-left-color: var(--frozen); }
.sf-stat-frozen .sf-stat-value { color: var(--frozen); }

/* ---- the four status tiles on an Activity page, on one row -----------------
   A modifier rather than a change to .sf-stats, which the Instructor Report
   also uses with a different number of tiles. Four fixed columns: at 360px
   that is four tiles of about 76px, which the auto-fit grid above would have
   wrapped into two rows of two. Everything inside is tightened to fit that
   width without the number -- the thing she is scanning for -- getting any
   smaller than it needs to be. */
.sf-stats--four {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: .4rem;
}

.sf-stats--four .sf-stat {
  min-width: 0;
  min-height: 44px;                       /* a real tap target */
  padding: .5rem .45rem .45rem .55rem;
  border-left-width: 4px;                 /* the colour reads at a glance */
}

.sf-stats--four .sf-stat-value { font-size: 1.45rem; }

/* The label has to sit on one line at 76px, which is why the wording was
   shortened in the view; the ellipsis is the guard if a translation ever
   makes one longer. */
.sf-stats--four .sf-stat-label {
  font-size: .62rem;
  letter-spacing: .04em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sf-stats--four .sf-stat-go { top: .35rem; right: .4rem; }

/* Nought. The tile keeps its status colour -- tint and border at full strength
   -- so the row always reads in the same four colours in the same order; only
   the digit goes to the muted grey, so a zero is seen and passed over rather
   than mistaken for a figure needing attention.

   Deliberately NOT opacity on the whole tile. Fading tint and text together at
   .55 measured 2.3-2.7:1 against the page, below even the large-text floor;
   the grey digit on the untouched tint measures 4.2-4.6:1 on all four, which a
   bold 23px number clears with room to spare in daylight. */
.sf-stat--zero .sf-stat-value { color: var(--muted); font-weight: 700; }
.sf-stat--zero .sf-stat-go { opacity: .3; }

/* ------------------------------------------------------- compact alert row */

/* One tappable line instead of a card with a heading and a full-width button.
   The whole strip is the link, so the sentence itself is what gets tapped. */
.sf-alert {
  display: flex;
  align-items: center;
  gap: .6rem;
  min-height: 56px;
  padding: .6rem .8rem;
  margin-bottom: 1rem;
  border-radius: var(--radius);
  background: var(--warn-bg);
  border: 1px solid var(--warn-line);
  border-left: 4px solid var(--accent);
  color: var(--warn-text);
  text-decoration: none;
  box-shadow: var(--shadow);
}
.sf-alert:active { filter: brightness(.97); }
.sf-alert:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

.sf-alert-icon { flex: 0 0 auto; width: 22px; height: 22px; }
.sf-alert-text { flex: 1 1 auto; font-size: .88rem; line-height: 1.35; }
.sf-alert-text strong { font-weight: 800; }
.sf-alert-go { flex: 0 0 auto; font-size: 1.1rem; opacity: .55; }

/* ---------------------------------------------------- dashboard backdrop */

/* THE DASHBOARD HAS NO GROUND OF ITS OWN ANY MORE.

   It used to layer two coloured washes over the shared background on this one
   screen, which is precisely what made the app's first page the one page that
   did not match the rest of it. The background is now declared once, on
   .sf-app .sf-frame::before, and the dashboard takes it unchanged like every
   other signed-in screen.

   The signed-in line that used to be darkened here needs no special case
   either: --muted itself is now deep enough to clear 4.5:1 against the darkest
   part of the ground, on this page and on every other.

   The cards, banner, tiles and meters on this screen are untouched -- they were
   never part of the ground. */

/* ------------------------------------------------------- instructor report */

/* Month picker and its button on one line, with the field taking the slack.
   The button matches the input's height so the two sit on the same baseline
   instead of the button floating below a taller field. */
.sf-month-form {
  display: flex;
  align-items: flex-end;
  gap: .5rem;
}
.sf-month-form .sf-field { flex: 1 1 auto; min-width: 0; }
.sf-month-go { flex: 0 0 auto; width: auto; min-height: 48px; padding: 0 1.1rem; }

/* ---------- the report period filter bar ----------------------------------

   WHY THIS WAS REWRITTEN. The old .sf-period-preset and .sf-period-mode asked
   for `border: 1.5px solid var(--line)`, and --line is a variable this app has
   never defined. An invalid custom property makes the whole shorthand invalid,
   so the border was dropped entirely and every chip rendered as bare bold text
   on a white card. That is what "it looks like plain links" was.

   Two lines, and only two: eight presets on a row that scrolls sideways by
   itself, then the mode segment, one date picker and Show. The ROW scrolls so
   the PAGE never has to. */

/* Tighter than a normal card. .sf-card's 1.25rem each side left 298px of a
   360px phone for a segmented control, a date box and a button that measured
   352 together -- the bar wrapped to three rows. Twelve gives it 312. */
.sf-perbar { display: flex; flex-direction: column; gap: .5rem; padding: .8rem .75rem; }

/* ---- line 1: the presets ---- */
.sf-perbar-presets {
  display: flex;
  gap: .35rem;
  overflow-x: auto;
  scrollbar-width: none;
  max-width: 100%;
  /* Room for the focus ring on the first and last chip, which a clipped
     overflow box would otherwise shave off. */
  padding: 2px;
  margin: -2px;
}
.sf-perbar-presets::-webkit-scrollbar { display: none; }

/* A chip. Outlined when off, filled when on -- and filled in --primary-strong
   rather than --primary. This file calls --primary "decorative fills with no
   text on them" and it means it: white on it measures 2.49:1, barely half the
   4.5 that text needs. --primary-strong carries white at 5.47:1. The old mode
   pill used --primary, so the one filled control on this bar was also the one
   piece of text on it nobody could read in sunlight. */
.sf-perbar-chip {
  flex: 0 0 auto;
  display: inline-flex; align-items: center;
  min-height: 44px; padding: 0 .75rem;
  border: 1.5px solid var(--border);
  border-radius: 999px;
  background: var(--card);
  color: var(--text);
  font-family: inherit; font-size: .8rem; font-weight: 700;
  white-space: nowrap; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.sf-perbar-chip:active { background: var(--surface-2); }
.sf-perbar-chip.is-on {
  border-color: var(--primary-strong);
  background: var(--primary-strong);
  color: #fff;
}

/* ---- line 2: mode, picker, go ---- */
.sf-perbar-line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .4rem;
}

/* The three modes as ONE control: joined, sharing their borders, exactly one
   filled. Three separate pills would read as three independent buttons that
   might all be off at once. */
.sf-perbar-seg {
  flex: 0 0 auto;
  display: inline-flex;
  border: 1.5px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
  background: var(--card);
}
.sf-perbar-segbtn {
  flex: 0 0 auto;
  min-height: 44px; padding: 0 .45rem;
  border: 0; border-left: 1.5px solid var(--border);
  background: transparent;
  color: var(--text);
  font-family: inherit; font-size: .72rem; font-weight: 800;
  white-space: nowrap; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.sf-perbar-segbtn:first-child { border-left: 0; }
.sf-perbar-segbtn:active { background: var(--surface-2); }
.sf-perbar-segbtn.is-on {
  background: var(--primary-strong);
  color: #fff;
}

/* The one picker the chosen mode uses. The caption sits above the box rather
   than beside it, so the pair stays narrow enough to share the line. */
/* Allowed to shrink well below its content: at 360px what is left after the
   segment and the button is about 116px, and a month box reads fine at that.
   Range is the exception -- two of these plus the segment cannot share one row
   on a 360px phone, so that mode wraps to a second row by design. */
.sf-perbar-date {
  flex: 1 1 5rem; min-width: 0;
  display: flex; flex-direction: column; gap: 1px;
}
.sf-perbar-cap {
  font-size: .62rem; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.sf-perbar-date input {
  width: 100%; min-width: 0; min-height: 40px;
  padding: 0 .4rem;
  border: 1.5px solid var(--border); border-radius: 10px;
  background: var(--card);
  color: var(--text); font-family: inherit; font-size: .8rem; font-weight: 700;
}

/* The primary action, and the only filled thing on the line besides the mode
   that is on. */
.sf-perbar-go {
  flex: 0 0 auto;
  min-height: 44px; padding: 0 .65rem;
  border: 0; border-radius: 999px;
  background: var(--primary-strong); color: #fff;
  font-family: inherit; font-size: .78rem; font-weight: 800;
  cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.sf-perbar-go:active { background: var(--primary-dark); }

/* The period in force, directly above the results it produced. */
.sf-perbar-showing {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: .3rem .5rem;
  margin: .8rem .15rem .6rem;
  font-size: .9rem;
  color: var(--muted);
}
.sf-perbar-showing strong {
  color: var(--text); font-weight: 800;
  font-variant-numeric: tabular-nums;
}
.sf-perbar-both { font-size: .72rem; }

/* What is actually on screen, said in words. "2026-08" in a box is not the
   same as reading "August 2026". */
.sf-month-showing { margin-top: .7rem; font-size: .875rem; }
.sf-month-showing strong { color: var(--text); }

/* The note under the totals. Quiet through colour, not through size: it is a
   sentence somebody has to read, so it stays at 14px like the rest of the body
   text. Only the uppercase micro-labels inside the tiles go smaller. */
.sf-fineprint { margin-top: .8rem; font-size: .875rem; line-height: 1.45; }

/* One instructor's card. It is a <details>, so the whole heading row is the
   control -- no separate button to hunt for on a phone. */
.sf-teacher { padding: 0; overflow: hidden; }

.sf-teacher-summary {
  display: flex;
  align-items: center;
  gap: .75rem;
  min-height: 64px;              /* well past the 44px tap minimum */
  padding: .8rem 1rem;
  cursor: pointer;
  list-style: none;
}
.sf-teacher-summary::-webkit-details-marker { display: none; }
.sf-teacher-summary:focus-visible { outline: 3px solid var(--primary); outline-offset: -3px; }
.sf-teacher-summary:active { background: var(--surface-2); }

.sf-teacher-who { flex: 1 1 auto; min-width: 0; }

.sf-teacher-name {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.3;
  overflow-wrap: anywhere;       /* a long name wraps rather than widening the page */
}

.sf-teacher-cover {
  display: block;
  margin-top: .15rem;
  font-size: .875rem;
  color: var(--muted);
}

/* The figure is the answer most days, so it gets the size and sits where the
   eye lands last on a left-to-right row. */
.sf-teacher-count {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .05rem;
  padding-left: .6rem;
  border-left: 1px solid var(--border);
}
.sf-teacher-figure {
  font-size: 1.6rem;
  font-weight: 800;
  line-height: 1.1;
  color: var(--primary-strong);
  font-variant-numeric: tabular-nums;
}
/* Matches .sf-stat-label rather than inventing a smaller size: the app already
   has one size for uppercase micro-captions and this is one of them. Every
   actual sentence on this page is 14px or larger. */
.sf-teacher-caption {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}

/* The chevron turns when the card opens, which is the only cue that the row
   was ever a control. */
.sf-teacher-summary::after {
  content: "";
  flex: 0 0 auto;
  width: 9px;
  height: 9px;
  margin-left: .1rem;
  border-right: 2px solid var(--muted);
  border-bottom: 2px solid var(--muted);
  transform: rotate(45deg);
  transition: transform .15s ease;
}
.sf-teacher[open] .sf-teacher-summary::after { transform: rotate(-135deg); }

@media (prefers-reduced-motion: reduce) {
  .sf-teacher-summary::after { transition: none; }
}

/* The class list behind the card. Each class is date, then what it was, then a
   cover tag if it was somebody else's -- wrapping rather than scrolling, which
   is the whole reason this is not a table. */
.sf-teacher-classes {
  list-style: none;
  margin: 0;
  padding: 0 1rem .9rem;
  border-top: 1px solid var(--border);
}

.sf-teacher-class {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .15rem .5rem;
  padding: .55rem 0;
  border-bottom: 1px solid var(--border);
  font-size: .88rem;
}
.sf-teacher-class:last-child { border-bottom: 0; padding-bottom: 0; }

.sf-teacher-date {
  flex: 0 0 auto;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.sf-teacher-what { flex: 1 1 9rem; min-width: 0; color: var(--muted); }
.sf-teacher-time { white-space: nowrap; }

/* ------------------------------------------------- instructors, compact */

/* A name and a number is all this card says, so it gets chips on a wrapping
   line rather than a full-width row each. The colours rotate purely to keep
   several people apart at a glance -- they carry no status meaning, which is
   why they are the soft tints and not the green/amber/red the app uses when a
   colour does mean something. */
.sf-teach {
  display: flex;
  flex-wrap: wrap;
  align-items: center;   /* the taller Payroll link must not stretch its neighbours */
  gap: .4rem;
}

.sf-teach-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-height: 32px;
  padding: .25rem .5rem .25rem .6rem;
  border: 1px solid transparent;
  border-radius: 999px;
  font-size: .8rem;
  line-height: 1.2;
}

/* The name can be long and there is not much width; it truncates rather than
   pushing the count off the chip. */
.sf-teach-name {
  max-width: 10.5rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

/* The count sits in its own disc, so the number reads as a number and not as
   part of the name. */
.sf-teach-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 20px;
  padding: 0 .3rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, .75);
  font-size: .72rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.sf-teach-cover {
  font-size: .68rem;
  font-weight: 700;
  opacity: .8;
}

.sf-teach-teal   { background: var(--surface-2); border-color: #99C7C2; color: var(--primary-strong); }
.sf-teach-violet { background: var(--violet-bg); border-color: #C4B5FD; color: var(--violet-text); }
.sf-teach-green  { background: var(--success-bg); border-color: var(--success-line); color: var(--success-text); }
.sf-teach-amber  { background: var(--warn-bg);   border-color: var(--warn-line);    color: var(--warn-text); }

/* The payroll link is the last chip. It is the only one that is tappable, so
   it is the only one that gets a tap target and a border people can see. */
a.sf-teach-go {
  min-height: 44px;
  padding: 0 .85rem;
  background: var(--card);
  border-color: var(--border);
  color: var(--primary-strong);
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
}
a.sf-teach-go:active { background: var(--surface-2); }
a.sf-teach-go:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* The V Techno Labs support account on Manage Users. Violet keeps it apart
   from the role badge beside it without reading as a warning -- the account is
   supposed to be there, it is just not one of the studio's own people. */
.sf-badge-support {
  background: var(--violet-bg);
  border: 1px solid #C4B5FD;
  color: var(--violet-text);
}

/* ============================================================
   Compact list header + single-line list rows
   Used by Batches and Plans. Layout only -- every action, URL and
   permission is exactly what it was when these were stacked cards.
   ============================================================ */

/* Title, count and the add button on one line. space-between keeps the button
   hard right at 360px without a width calculation. */
.sf-listhead {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: .75rem;
  margin: 0 0 .9rem;
}

.sf-listhead-title {
  display: flex;
  align-items: center;
  gap: .55rem;
  min-width: 0;              /* lets the text shrink instead of shoving the button */
}

/* Explicitly sized. An SVG with no width/height renders at 300x150. */
.sf-listhead-icon {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  color: var(--primary-strong);
}

.sf-listhead-text { min-width: 0; }
.sf-listhead-text h1 { margin: 0; font-size: 1.35rem; font-weight: 800; line-height: 1.2; }

.sf-listhead-count {
  margin: 0;
  font-size: .78rem;
  color: var(--muted);
  white-space: nowrap;
}

/* Icon-only add button. The label lives in aria-label and title, so a screen
   reader and a long-press both still say "Add batch". */
/* 40px of visible button inside a 44px tap target. The transparent border does
   the work: background-clip keeps the amber inside the padding box, so the
   button still LOOKS 40px while a thumb gets the 44px it needs. */
.sf-listhead-add {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 2px solid transparent;
  background-clip: padding-box;
  box-sizing: border-box;
  border-radius: 14px;
  background-color: var(--accent);
  color: var(--accent-text);
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1;
  text-decoration: none;
}
.sf-listhead-add:active { background-color: var(--accent-dark); }
.sf-listhead-add:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* ---------------------------------------------------------- one-line rows */

/* The whole list is one card-like surface with dividers, rather than a card per
   item: seven separate cards is what made this page so long. */
.sf-listrows {
  background: var(--card);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
  margin-bottom: 1rem;
}

.sf-listrow { border-top: 1px solid var(--border); }
.sf-listrow:first-child { border-top: 0; }

/* The row itself. It is a <summary> when there are actions behind it and a
   plain div when there are not, so both are styled here. */
.sf-listrow-line {
  display: flex;
  align-items: center;
  gap: .6rem;
  min-height: 48px;
  padding: .5rem .75rem;
  cursor: pointer;
  list-style: none;
}
.sf-listrow-line::-webkit-details-marker { display: none; }
.sf-listrow-line:focus-visible { outline: 3px solid var(--primary); outline-offset: -3px; }
details.sf-listrow > .sf-listrow-line:active { background: var(--surface-2); }

/* A plain row has nothing to open, so it must not look like it does. */
.sf-listrow-static > .sf-listrow-line { cursor: default; }

/* Fixed width and tabular figures, so every start time sits on the same column
   down the list and the eye can run straight down it. */
.sf-listrow-time {
  flex: 0 0 auto;
  width: 5.6rem;
  font-size: .82rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  color: var(--text);
}

/* The flexible middle. min-width:0 is what actually allows the ellipsis --
   without it a flex child refuses to shrink below its content. */
.sf-listrow-main { flex: 1 1 auto; min-width: 0; }

.sf-listrow-name {
  display: block;
  font-size: .9rem;
  font-weight: 600;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Instructor, capacity and any state chip share one quiet second line. It
   truncates as a whole, so a long instructor name can never push the badge or
   the chevron off the screen. */
.sf-listrow-sub {
  display: block;
  font-size: .75rem;
  line-height: 1.35;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sf-listrow-sub .sf-listrow-warn { color: var(--danger-text); font-weight: 700; }

/* Badge and chevron never shrink -- they are the fixed right edge the middle
   column truncates against. */
.sf-listrow-badge { flex: 0 0 auto; }

.sf-listrow-caret {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
}
.sf-listrow-caret svg { width: 16px; height: 16px; }
details.sf-listrow[open] .sf-listrow-caret { transform: rotate(180deg); }

@media (prefers-reduced-motion: no-preference) {
  .sf-listrow-caret { transition: transform .15s ease; }
}

/* Edit / Stop, revealed under the row rather than stacked below every item all
   the time. Same links, same forms, same permissions -- just folded away. */
.sf-listrow-acts {
  display: flex;
  flex-wrap: wrap;
  gap: .45rem;
  padding: 0 .75rem .7rem calc(5.6rem + 1.35rem);
  background: var(--surface-2);
}
.sf-listrow-acts .sf-inline-form { display: inline-flex; }

/* A stopped batch or retired plan stays readable but visibly stands down. */
.sf-listrow-off .sf-listrow-time,
.sf-listrow-off .sf-listrow-name { color: var(--muted); }

/* At the narrowest widths the action row loses its indent -- lining up under
   the name matters less than the buttons having room. */
@media (max-width: 380px) {
  .sf-listrow-acts { padding-left: .75rem; }
  .sf-listrow-time { width: 5.1rem; font-size: .78rem; }
}

/* ------------------------------------------------------ members list rows */

/* The row link and the Renew shortcut sit side by side. A link cannot be nested
   inside another link, so they are siblings and the row takes the slack. */
.sf-memberrow { display: flex; align-items: stretch; }
.sf-memberrow > .sf-listrow-line { flex: 1 1 auto; min-width: 0; text-decoration: none; color: inherit; }

/* Small, quiet, and only rendered for members who actually need renewing. */
.sf-memberrow-renew {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  padding: 0 .7rem;
  margin: .35rem .5rem .35rem 0;
  border: 1px solid var(--accent);
  border-radius: 999px;
  background: var(--warn-bg);
  color: var(--accent-dark);
  font-size: .72rem;
  font-weight: 700;
  white-space: nowrap;
  text-decoration: none;
}
.sf-memberrow-renew:active { background: var(--warn-line); }
.sf-memberrow-renew:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }

/* The fixed right-hand column: status above, expiry below. It never shrinks --
   it is the edge the name and the sub-line truncate against. */
.sf-memberside {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .15rem;
}

.sf-memberside-exp {
  display: inline-flex;
  align-items: center;
  gap: .2rem;
  font-size: .7rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A compact status chip. The full-size pill is too tall for a 48px row. */
.sf-pill-sm {
  min-height: 20px;
  padding: 0 .45rem;
  font-size: .66rem;
  line-height: 1.7;
}

/* Money owed, said with a dot rather than a figure: the amount belongs on the
   member's own screen; this only has to say that something is outstanding. */
.sf-dues-dot {
  color: var(--danger);
  font-size: 1.1rem;
  line-height: 0;
  position: relative;
  top: .1em;
}

/* The letter headings down the list. Deliberately its own class rather than
   .sf-group-head, which is declared twice in this file already (once as a small
   uppercase label, once as a 52px tappable header on the payments list) and
   would bring the second one's height along with it. */
/* A letter divider inside the list rather than a heading above a separate
   card. Nine initials would otherwise mean nine cards, each with its own gap
   and shadow around a single member. */
.sf-rowgroup,
.sf-azgroup {
  padding: .3rem .75rem;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--muted);
}
.sf-rowgroup:first-child,
.sf-azgroup:first-child { border-top: 0; }

/* A row straight after a divider does not need a second line above it. */
.sf-rowgroup + .sf-listrow,
.sf-azgroup + .sf-listrow { border-top: 0; }

/* Pager. Only rendered when there is more than one page. */
.sf-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin: 0 0 1rem;
}
.sf-pager-at {
  font-size: .78rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* =============================================================
   Instructor dashboard — compact sections and rows
   Layout only. Every link, figure and permission is unchanged.
   ============================================================= */

/* A section heading and its summary on one line: title left, counts right.
   Replaces an h2 with a paragraph under it, which cost two lines to say what
   fits on one. */
.sf-sechead {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: .6rem;
  margin: 0 0 .55rem;
}

.sf-sechead h2 {
  margin: 0;
  font-size: .78rem;
  font-weight: 800;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--muted);
}

.sf-sechead-note {
  margin: 0;
  flex: 0 0 auto;
  font-size: .75rem;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The activity badge sits on the row's second line. Its own line-height would
   otherwise make the row taller than the rest, so it is pinned back. */
.sf-classrow .sf-listrow-sub { line-height: 1; margin-top: .1rem; }

/* The right-hand end of a class row: state chip, then the register link if
   there is one to offer. */
.sf-rowact {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .35rem;
}

/* A compact icon link instead of a full-width button on its own line. 44px so
   it is still a real tap target on a 48px row. */
.sf-rowact-go {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-right: -.35rem;      /* pulls the padding back so the row stays tight */
  border-radius: 10px;
  color: var(--primary-strong);
  text-decoration: none;
}
.sf-rowact-go svg { width: 18px; height: 18px; }
.sf-rowact-go:active { background: var(--surface-2); }
.sf-rowact-go:focus-visible { outline: 3px solid var(--primary); outline-offset: -3px; }

/* The open register is the one thing worth shouting about, so its link is the
   only filled one. */
.sf-rowact-open {
  background: var(--accent);
  color: var(--accent-text);
}
.sf-rowact-open:active { background: var(--accent-dark); }

/* Said once under the list rather than under every locked row, where it cost a
   line each and read like a different message every time. */
.sf-listnote {
  margin: -.6rem 0 1rem;
  padding: 0 .2rem;
  font-size: .75rem;
  line-height: 1.45;
  color: var(--muted);
}

/* Two figures on one line instead of two full-width rows with headings. */
.sf-statpills {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem;
}

.sf-statpill {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  padding: .35rem .65rem;
  border-radius: 999px;
  background: var(--surface-2);
  font-size: .76rem;
  color: var(--muted);
}
.sf-statpill strong {
  font-size: .95rem;
  font-weight: 800;
  color: var(--primary-strong);
  font-variant-numeric: tabular-nums;
}

/* One expiring member per row, the whole row a single link to their details. */
.sf-exprow > .sf-listrow-line { text-decoration: none; color: inherit; }

.sf-exprow-side {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .1rem;
}
.sf-exprow-when {
  font-size: .7rem;
  color: var(--muted);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* The same small note used as a lead-in above a list rather than a follow-up
   below one, so it needs the negative top margin taken back off. */
.sf-listnote-lead { margin-top: 0; margin-bottom: .55rem; }

/* The 44px register icon plus the row's own padding made those rows 60px while
   the rest sat at 49. Trimming the vertical padding on class rows only brings
   them to 52 against 48 -- the min-height still guarantees the tap target, and
   the list reads as one rhythm rather than two. */
.sf-classrow > .sf-listrow-line { padding-top: .25rem; padding-bottom: .25rem; }

/* ------------------------------------------------- payments period selector */

/* Three cards on one row. They stay three at 360px -- the whole point is to read
   a month against the year at a glance, which needs all three visible. */
.sf-periods {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .5rem;
  margin-bottom: 1rem;
  align-items: start;
}

.sf-period {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-height: 62px;
  padding: .55rem .6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--text);
  text-decoration: none;
  box-shadow: var(--shadow);
}
.sf-period:active { background: var(--surface-2); }
.sf-period:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* The chosen one is the only filled card, and the only one that spans the row:
   its breakdown needs the width, and grid-column is what lets it take it
   without the other two moving. */
.sf-period-on {
  grid-column: 1 / -1;
  order: -1;
  background: var(--success-bg);
  border-color: var(--success-line);
  border-left: 4px solid var(--success);
}

.sf-period-label {
  font-size: .66rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sf-period-figure {
  font-size: 1rem;
  font-weight: 800;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}
.sf-period-on .sf-period-figure { font-size: 1.6rem; color: var(--success-text); }

.sf-period-detail {
  margin-top: .15rem;
  font-size: .76rem;
  line-height: 1.4;
  color: var(--text);
}

/* Waived sits on its own line, in the amber the app uses for money that was
   forgiven rather than received. It is never part of the figure above. */
.sf-period-waived {
  margin-top: .1rem;
  font-size: .74rem;
  line-height: 1.4;
  font-weight: 700;
  color: var(--warn-text);
}

/* A section heading whose right-hand note is a link rather than static text. */
.sf-sechead-link {
  color: var(--primary-strong);
  font-weight: 700;
  text-decoration: none;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
.sf-sechead-link:focus-visible { outline: 3px solid var(--primary); outline-offset: 2px; }

/* The chevron is an SVG now, so it needs a box rather than a font size. */
.sf-group-chevron {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  color: var(--muted);
}
.sf-group-chevron svg { width: 16px; height: 16px; }
.sf-group-open > .sf-group-head .sf-group-chevron { transform: rotate(180deg); }

@media (prefers-reduced-motion: no-preference) {
  .sf-group-chevron { transition: transform .15s ease; }
}

/* Written off, not received: amber, and marked as an addition to nothing. */
.sf-payrow-waived { color: var(--warn-text); }

/* A hand-typed range gets the same breakdown the selected card carries, so
   choosing your own dates does not lose the figures. */
.sf-period-custom { border-left: 4px solid var(--primary-strong); }
.sf-period-figure-lg { font-size: 1.6rem; color: var(--primary-strong); }

/* --------------------------------------------- per-batch attendance counts */

/* A row line that is a link must not look like body text with a blue underline
   through it. Applies wherever a compact row is tappable. */
a.sf-listrow-line { text-decoration: none; color: inherit; }

/* The counts sit in the fixed right-hand column and never shrink -- they are
   the figures the row exists for, so the batch name truncates around them
   rather than them being clipped. */
.sf-countbox {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .15rem;
}

.sf-counts {
  display: flex;
  align-items: center;
  gap: .2rem;
  white-space: nowrap;
}

/* Small, fixed, and never wrapping: these are the figures the row exists for,
   so they hold their width and the name truncates around them. */
.sf-count {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  padding: 0 .3rem;
  border-radius: 5px;
  font-size: .68rem;
  font-weight: 700;
  line-height: 1.7;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Enrolled: the denominator, so it stays quiet. */
.sf-count-total {
  background: var(--surface-2);
  color: var(--muted);
}

/* Present green and absent red, the same two meanings they carry everywhere
   else in the app. Both measured on their own tint: 5.9:1 and 6.2:1. */
.sf-count-p {
  background: var(--success-bg);
  color: var(--success-text);
}

.sf-count-a {
  background: var(--danger-bg);
  color: var(--danger-text);
}

/* "Not marked" and "Cancelled": a state, not a number, so no colour that would
   read as good or bad news. */
.sf-count-off {
  background: var(--surface-2);
  color: var(--muted);
  font-weight: 600;
}

/* The one thing the counts cannot say, and the one worth acting on. */
.sf-count-open {
  background: var(--primary-strong);
  color: var(--primary-text);
  letter-spacing: .04em;
}

/* ------------------------------------------------------ dashboard: the day */

/* The date leads the screen, because everything under it is about today. */
.sf-today { margin: 0 0 .35rem; }

.sf-today-date {
  margin: 0;
  font-size: 1.15rem;
  font-weight: 800;
  line-height: 1.25;
  color: var(--text);
}

.sf-today-sum {
  margin: .1rem 0 0;
  font-size: .85rem;
  color: var(--muted);
}
.sf-today-sum strong { color: var(--primary-strong); font-weight: 800; }

/* ------------------------------------------------- dashboard: the icon row */

/* Scrolls sideways on its own so the page never does. */
.sf-tiles {
  display: grid;

  /* Three per row, always. Equal fractions rather than three fixed 64px columns
     so the tiles distribute evenly across whatever width the phone gives, and
     the second row lines up under the first on every screen. The number of rows
     follows from how many tiles the caller passed: six become 2 x 3, three
     become 1 x 3, from this one rule. */
  grid-template-columns: repeat(3, 1fr);
  justify-items: center;
  gap: .875rem;                 /* 14px, between columns and between rows */

  margin: 0 0 1.1rem;
  padding-bottom: .2rem;
}

.sf-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .3rem;
  width: 64px;
  text-decoration: none;
  color: var(--muted);
}

/* 64px of tile, as specified. The glyph inside is sized explicitly too, so a
   failure anywhere upstream cannot leave an SVG at its 300x150 default. */
.sf-tile-box {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 64px;
  height: 64px;
  border-radius: 18px;
  color: #FFFFFF;
  box-shadow: var(--shadow);
}
.sf-tile-box img { width: 32px; height: 32px; display: block; }

.sf-tile:active .sf-tile-box { filter: brightness(.92); }
.sf-tile:focus-visible .sf-tile-box { outline: 3px solid var(--primary); outline-offset: 3px; }

.sf-tile-label {
  font-size: .68rem;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  max-width: 64px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --------------------------------------------- dashboard: account actions */

/* Change password and Log out together at the foot of the page: both are about
   the account rather than the studio, so they belong side by side. */
/* Stacked, not side by side. Two 10rem buttons plus the gap came to exactly the
   content width at 360px, so "Change password" broke onto a second line and read
   as two buttons. Full width each is what Log out looked like before this block
   existed, and it cannot wrap. */
.sf-account {
  display: flex;
  flex-direction: column;
  gap: .5rem;
  margin-top: 1.25rem;
}
.sf-account > *, .sf-account form { margin: 0; }
.sf-account .sf-btn { width: 100%; }

/* ======================================================= the app icon set */

/* One file per module, painted through a mask so the colour is inherited.
   background-color is the visible paint; the SVG only supplies the shape. That
   is why an icon can be white on a dashboard tile and teal in a page header
   without a second copy of the file existing anywhere.

   Both sizes are fixed here. Nothing in this set is ever rendered unsized. */
.sf-ico {
  display: inline-block;
  flex: 0 0 auto;
  background-color: currentColor;
  -webkit-mask: var(--sf-ico) center / contain no-repeat;
  mask: var(--sf-ico) center / contain no-repeat;
}

/* Two sizes, and only two. There was a 24px step as well, which left page
   headers at 24px on seven screens and 28px on the two the briefs specified --
   the same thing at two sizes depending on which week it was built. Everything
   is 28px now, matching the supplied Manage Users artwork, which is fixed. */
.sf-ico-md { width: 28px; height: 28px; }   /* page headers */
.sf-ico-lg { width: 32px; height: 32px; }   /* inside a 64px dashboard tile */

/* The glyph on a coloured tile. White comes from here, not from the file. */
.sf-tile-box { color: #FFFFFF; }

/* The glyph beside a page title, in the app's own teal. */
.sf-listhead-icon.sf-ico { color: var(--primary-strong); }

/* A page head that is not the two-line list header still gets its icon on the
   title's line rather than above it. */
.sf-page-head-icon {
  display: flex;
  align-items: center;
  gap: .55rem;
}
.sf-page-head-icon .sf-ico { color: var(--primary-strong); }

/* An icon that arrives as a finished tile — its own coloured square and its own
   corner radius — is placed as-is. No background behind it, no recolouring, and
   no mask: a mask reads only alpha, and this artwork is opaque across the whole
   64x64, so it would paint as a solid block. */
.sf-tile-img {
  display: block;
  width: 64px;
  height: 64px;
  border-radius: 15px;      /* matches the rx in the artwork, for the shadow */
  box-shadow: var(--shadow);
}

/* The drawn tiles match that artwork's 15px rather than the other way round:
   the supplied file is fixed, so everything else lines up with it. */
.sf-tile-box { border-radius: 15px; }

/* The Manage Users mark beside a page title. It is a tile, not a line glyph, so
   it carries its own colour and needs no currentColor. Sized on the element as
   well as in the attributes, so nothing can leave it unsized. */
.sf-head-img {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 7px;       /* 15/64 of 28, so the corners scale true */
  display: block;
}

/* The supplied Dashboard mark in the bottom bar. Sized on the element as well
   as in the attributes, so nothing can leave it unsized, and pinned so the
   active state cannot scale or distort it. */
.sf-tab-img {
  width: 24px;
  height: 24px;
  border-radius: 6px;        /* 15/64 of 24, so its corners scale true */
  display: block;
  flex: 0 0 auto;
}

/* The active pill is deliberately left alone for this tab. Suppressing it would
   have made Dashboard the one tab whose selected state looks different from the
   other four, and the pill is the strongest cue in the bar. The cost is that an
   orange tile sits inside a teal lozenge when the tab is active, which is a
   consequence of the artwork being a finished tile rather than a glyph. */

/* Spacing is the grid's job now: repeat(3, 1fr) with justify-items: center
   spreads the tiles evenly by construction, so the flex override that used to
   do it has been removed rather than left to sit inert. */

/* ============================================ membership plan add / edit form

   Prefixed sf-plan-* throughout so nothing here can reach another form. The
   shared field, input and sticky-save classes are reused rather than
   redefined -- this screen should look like the app, not like itself. */

/* Comfortable padding and one rhythm between fields, rather than whatever each
   control happened to bring. */
.sf-plan-form { padding: 1.15rem 1rem 1.25rem; }
.sf-plan-form .sf-field { margin-bottom: 1.05rem; }
.sf-plan-form .sf-field:last-child { margin-bottom: 0; }

/* The hint under a field belongs to that field, so it sits tight to it and the
   rhythm is kept by the field's own margin. */
.sf-plan-form .sf-hint { margin: .3rem 0 0; }
.sf-plan-form .sf-field-error { margin-top: .3rem; }

/* Duration and price share a row. Top-aligned so the duration shortcuts cannot
   drag the price input down to meet them. */
.sf-plan-pair { align-items: flex-start; }

/* Two columns is worse than one below this width: at 360px each would be about
   150px, and a number with a unit inside it does not fit that. */
@media (max-width: 379px) {
  .sf-plan-pair { flex-direction: column; gap: 0; }
  .sf-plan-pair > .sf-field { width: 100%; }
}

/* The unit inside the field, mirroring the price prefix from the other end. */
.sf-plan-suffix {
  position: absolute;
  right: .85rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  font-size: .82rem;
  pointer-events: none;      /* the tap belongs to the input underneath */
}

/* Room for it, plus a little more: a number input also carries its spinner. */
.sf-plan-suffixed { padding-right: 4.6rem; }

/* The description. Same box as every other input, just taller. */
.sf-plan-note {
  min-height: 5.5rem;
  padding-top: .7rem;
  padding-bottom: .7rem;
  line-height: 1.45;
  resize: vertical;
}

/* Read-only context above the fields: how many people this change reaches. */
.sf-plan-inuse {
  margin: 0 0 1.05rem;
  padding: .55rem .7rem;
  border-left: 3px solid var(--primary-strong);
  border-radius: 8px;
  background: var(--surface-2);
  font-size: .85rem;
  font-weight: 600;
  color: var(--text);
}

/* ---- the availability switch ----

   The same checkbox as before, restyled. The input keeps its name, value and
   binding and is only visually hidden, so the form posts exactly what it did.
   Hidden with opacity rather than display:none, because a display:none control
   cannot be focused and the keyboard would lose it. */
.sf-plan-switch-field { padding-top: .15rem; }

.sf-plan-switch {
  display: flex;
  align-items: center;
  gap: .65rem;
  min-height: 44px;
  cursor: pointer;
}

.sf-plan-switch-input {
  position: absolute;
  opacity: 0;
  width: 44px;
  height: 44px;
  margin: 0;
  cursor: pointer;
}

.sf-plan-switch-track {
  flex: 0 0 auto;
  position: relative;
  width: 46px;
  height: 28px;
  border-radius: 999px;
  background: var(--border);
  transition: background .15s ease;
}

.sf-plan-switch-track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--card);
  box-shadow: 0 1px 3px rgba(15, 23, 42, .3);
  transition: transform .15s ease;
}

.sf-plan-switch-input:checked ~ .sf-plan-switch-track { background: var(--success); }
.sf-plan-switch-input:checked ~ .sf-plan-switch-track::after { transform: translateX(18px); }

/* The focus ring has to land on the track, since the input itself is invisible.
   Without this the switch is unusable from a keyboard. */
.sf-plan-switch-input:focus-visible ~ .sf-plan-switch-track {
  outline: 3px solid var(--primary);
  outline-offset: 2px;
}

.sf-plan-switch-label { font-size: .95rem; font-weight: 600; }
.sf-plan-switch-hint { margin-top: .45rem; }

@media (prefers-reduced-motion: reduce) {
  .sf-plan-switch-track, .sf-plan-switch-track::after { transition: none; }
}

/* ---- the action row ---- */

/* Save leads and takes the width it needs; Cancel sits beside it and quieter.
   Both are full height, so neither is a small target at the foot of a form. */
.sf-plan-actions {
  display: flex;
  gap: .6rem;
}
.sf-plan-actions .sf-btn {
  flex: 1 1 0;
  min-height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 0;
}

/* Save is the wider of the two: it is what this screen is for. */
.sf-plan-actions .sf-btn-accent { flex: 2 1 0; }

/* The duration shortcuts sit inside a column about 156px wide, so they have to
   fit four across without wrapping -- a wrapped second row pushed this field
   below the price beside it and the pair stopped looking like a pair. */
.sf-plan-chips { gap: .3rem; flex-wrap: nowrap; }
.sf-plan-chip { min-width: 34px; padding: 0 .45rem; }

/* The shortcuts now hang under the duration input, so they need the small gap
   that used to sit between them and it. */
.sf-plan-chips { margin-top: .4rem; margin-bottom: 0; }

/* ------------------------------------------------------- activity artwork */

/* A badge that carries an icon becomes a flex line so the mark and the word sit
   on one baseline and never wrap apart. Badges without artwork keep the plain
   inline-block above, so the two look like the same control either way. */
.sf-activity-iconed {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding-left: .22rem;      /* the tile brings its own edge, so less is needed */
}

/* The tile is supplied with its own cream background and its own rx, so it is
   placed rather than recoloured. The radius here only matches the artwork so
   the corners scale true at whatever size the caller asked for. */
.sf-activity-icon {
  flex: 0 0 auto;
  display: block;
  border-radius: 23.4%;      /* 15/64, so it holds at 20px and at 28px alike */
}

/* The label keeps the badge's own type; naming it lets the icon sit beside it
   without inheriting the flex gap as letter-spacing. */
.sf-activity-name { display: inline-block; }

/* The Payments mark on the dashboard money tile. Small and set back a little:
   the line it shares is a 10px affordance, not a heading, so the icon sits with
   it rather than over it. */
.sf-money-tile-icon {
  width: 20px;
  height: 20px;
  border-radius: 5px;        /* 15/64 of 20, so the corners scale true */
  vertical-align: -.35em;
  margin-right: .25rem;
}

/* ------------------------------------------------------- activity detail */

/* Six tiles fall into two rows of three and three into one row, from the same
   grid: the column count is fixed at three, so the row count follows from how
   many tiles there are. No wrap rule to tune, and nothing left to scroll. */

/* One instructor's share of a batch this month. */
.sf-actwho { white-space: nowrap; }
.sf-actwho strong { font-weight: 800; color: var(--text); }

/* The batch's own instructor carries a dot, so a cover reads as a cover rather
   than as the usual name. A colour alone would not say which is which. */
.sf-actwho-assigned::before {
  content: "";
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-right: .25rem;
  border-radius: 50%;
  background: var(--primary-strong);
  vertical-align: .12em;
}

/* The roster figure sits at the row's right edge and never shrinks, so a long
   batch name truncates around it. */
.sf-actbatch .sf-countbox { align-items: flex-end; }

/* ------------------------------------------------------- uppercase entries

   The studio stores names in capitals -- see Data/UppercaseTextPolicy.cs, which
   is what actually does it, on every save. This class is only so the admin sees
   what she is about to store while she is still typing it.

   text-transform changes what is PAINTED, never the value in the box: the form
   still posts exactly what was typed, and the database layer is what shouts it.
   The two agree, so the field never shows one thing and saves another.

   Put it only on the fields that policy covers -- first name, last name,
   address, full name, batch name, plan name. Never on a username, an email, a
   phone number or a free-text note: those are stored as typed, and styling them
   as capitals would promise something the save does not do. */
.sf-upper { text-transform: uppercase; }

/* ::placeholder is exempt on purpose. A hint reads as a hint -- "e.g. Sneha
   Kulkarni" in capitals looks like a value that is already there. */
.sf-upper::placeholder { text-transform: none; }

/* ================================================================ error pages

   The 404 / 403 / 500 screens. They reuse .sf-card, .sf-h1 and .sf-btn like
   every other page, so a failure still looks like StudioFlow rather than like
   the app falling over; these few rules only set the centred rhythm and make
   the reference id readable enough to be read down a phone. */
.sf-error {
  text-align: center;
  max-width: 30rem;
  margin: 1.5rem auto;
}
.sf-error-title { margin: 0 0 .5rem; }
.sf-error-lead {
  margin: 0 0 1rem;
  font-size: 1rem;
  line-height: 1.5;
  text-wrap: pretty;
}
.sf-error-support { margin: 0 0 .75rem; font-size: .9rem; }
.sf-error-contact { margin: 0 0 1rem; }

/* The one string worth quoting to support: monospaced and boxed so it cannot be
   misread, and selectable so it can be copied rather than transcribed. */
.sf-error-ref {
  margin: 0 0 1.25rem;
  font-size: .85rem;
  color: var(--muted);
}
.sf-error-ref code {
  padding: .15rem .45rem;
  border-radius: 6px;
  background: rgba(43, 42, 31, .07);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-weight: 700;
  letter-spacing: .08em;
  color: var(--text);
  user-select: all;
}

/* ============================================================ header ⋮ menu

   Anchored to the app bar rather than to the page. The bar is fixed and carries
   a transform, which makes it the containing block for anything positioned
   inside it -- so the panel cannot be clipped by the scrolling column beneath,
   and it lines up with the frame on a laptop without a second width rule.

   Rows come from HeaderMenu, so a new item is an entry in that list rather than
   a change here. Prefix sf-menu- throughout. */
.sf-menu { position: relative; }

.sf-menu-btn { /* the ⋮ itself; sizing and tap target come from .sf-icon-btn */ }

.sf-menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 4px;
  z-index: 30;                  /* above the bar's own 20 */
  min-width: 208px;
  padding: 6px;
  border-radius: 14px;
  background: #ffffff;
  border: 1px solid rgba(43, 42, 31, .12);
  box-shadow: 0 18px 40px -16px rgba(43, 42, 31, .45);
}

/* One row. min-height 44px is the tap target, not decoration: this menu is
   used on a phone, one-handed, often mid-class. */
.sf-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 44px;
  padding: 0 12px;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.sf-menu-item img { flex: none; border-radius: 4px; display: block; }
.sf-menu-item:hover,
.sf-menu-item:focus-visible { background: rgba(43, 42, 31, .06); }

/* Ending the session reads as what it is, and only after a rule. */
.sf-menu-item--danger { color: #b3261e; }
.sf-menu-item--danger:hover,
.sf-menu-item--danger:focus-visible { background: #fdecea; }

.sf-menu-sep {
  height: 1px;
  border: 0;
  margin: 6px 8px;
  background: rgba(43, 42, 31, .12);
}

/* A group label -- "Reports" above the three report rows.

   Deliberately small, quiet and not a tap target: it names the rows beneath it
   and is never mistaken for one of them. The indent lines it up with the row
   labels rather than the panel edge, so the group reads as a column. */
.sf-menu-heading {
  margin: 6px 0 2px;
  padding: 0 12px;
  font-size: .7rem;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* The panel's own warm grey, not --muted: that token is a cool blue-grey and
     would be the one cool thing inside a warm panel. */
  color: rgba(43, 42, 31, .55);
}

/* A posting row is a form, which is a block; this keeps it the shape of a row. */
.sf-menu-form { margin: 0; display: block; }

/* ---- a member already Present in another class today ---------------------
   The register's own note, the line that totals them, and the confirmation
   that has to be ticked before one of them can be marked twice. All three sit
   in the existing register card, so nothing here changes its shape. */

/* "0 present · 2 attended another class" — the owner's line. */
.sf-register-away {
  margin: -.35rem 0 .6rem;
  font-size: .8rem;
  font-weight: 700;
  color: var(--accent-dark);
}

/* WAS the register's double-mark confirmation. A save that would have marked
   somebody Present in two classes at once is no longer refused and offered a
   tick; the one member is dropped and the rest of the register saves. The rules
   below style the block that replaced it: a search result that cannot be added.

   Red, not amber. Amber was right for "waiting to be told this was deliberate";
   this is a refusal, and there is nothing to decide. */
.sf-visitor-why {
  margin: 3px 0 0;
  font-size: .78rem; font-weight: 700; color: var(--danger-text);
  text-wrap: pretty;
}

/* A member who cannot be added. Faded and with no button beside them, so the
   row reads as unavailable before the sentence explaining it is read -- but
   never faded so far that the name and the reason stop being legible. */
.sf-register-row--blocked { opacity: .72; }
.sf-register-row--blocked .sf-register-name { color: var(--muted); }
.sf-register-row--blocked .sf-visitor-why { opacity: 1; }

/* The same fact on a dashboard batch row, in that row's own muted voice. */
.sf-dash-away { color: rgba(255, 255, 255, .78); font-weight: 700; }

/* ---- the visitor marker -------------------------------------------------

   A MEMBER OF ANOTHER CLASS, HERE FOR ONE SESSION. They hold no place in this
   batch, so they are never inside the capacity fraction beside them: a class of
   twenty with two guests reads "20/20 · 2V", not "22 of 20".

   Deliberately unlike the count chip it sits next to. That one is a fraction in
   the app's ordinary ink; this is a small filled pill in the visitor accent, so
   the eye reads two figures rather than one number split by a dot. The letter V
   is the whole label because the row is 360px wide and already carries a time, a
   capacity and a state -- the sentence lives on the title attribute. */
.sf-count-visitor {
  background: var(--accent);
  color: var(--accent-text);
  font-weight: 800;
  white-space: nowrap;
}

/* The same marker on a dashboard class row, in that row's own voice. */
.sf-dash-visitor {
  display: inline-flex; align-items: center;
  padding: 0 .4rem; border-radius: 999px;
  background: rgba(255, 255, 255, .18);
  color: #fff; font-size: .72rem; font-weight: 800;
  white-space: nowrap;
}

/* The register heading: the count and the visitor marker share the right-hand
   slot, so a long pair still wraps as a unit instead of splitting the fraction
   away from its own marker. */
.sf-register-heading-counts {
  display: inline-flex; align-items: center; gap: .35rem; flex-wrap: wrap;
  justify-content: flex-end;
}
