/* =====================================================================
   f7_bridge.css — Framework7 theme bridge for FamilieRutiner
   ---------------------------------------------------------------------
   Purpose: carry the Islamic/Omar-og-Hana visual identity that lives in
   islamic_theme.css (--sky, --yellow, --coral, Nunito, Amiri) into the
   Framework7 component layer, by overriding the --f7-* custom properties
   that Framework7 reads.

   Load ORDER matters: islamic_theme.css defines the brand tokens, this
   file consumes them. Load this file AFTER islamic_theme.css.

   Every --f7-* name used here was verified to exist in the shipped
   shinyMobile 2.0.1 stylesheet (1045 variables), not guessed.
   ===================================================================== */

:root {
  /* ---- Core theme colour ------------------------------------------- */
  /* Framework7 derives buttons, links, active tabs, toggles, progress
     bars and ripples from these. --f7-theme-color-rgb must stay in sync
     with --f7-theme-color or translucency effects use the wrong hue.    */
  --f7-theme-color:              #007A94;   /* --sky-dark */
  --f7-theme-color-rgb:          0, 122, 148;
  --f7-theme-color-shade:        #005E72;
  --f7-theme-color-tint:         #008196;   /* --sky */
  --f7-theme-color-text-color:   #007A94;
  --f7-theme-color-bg-color:     #007A94;
  --f7-theme-color-border-color: #007A94;
  --f7-theme-color-ripple-color: rgba(0, 122, 148, 0.18);

  /* ---- Typography --------------------------------------------------- */
  /* Nunito for Danish, Amiri is applied per-element by islamic_theme.css
     on [dir="rtl"] and .ar text, so it is not set globally here.        */
  --f7-font-family: 'Nunito', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
  --f7-font-size:   16px;
  --f7-text-color:  #1A3040;              /* --text */

  /* ---- Page surface ------------------------------------------------- */
  --f7-page-bg-color:  #F5FCFF;           /* --bg */
  --f7-block-text-color: #1A3040;
  --f7-block-strong-bg-color: #FFFFFF;    /* --surface */
  --f7-block-strong-text-color: #1A3040;
  --f7-block-title-text-color: #007A94;
  --f7-block-title-font-weight: 900;
  --f7-block-title-text-transform: none;  /* Danish headings are sentence case */
  --f7-block-inset-border-radius: 18px;   /* --radius */

  /* ---- Navbar (top bar) --------------------------------------------- */
  /* The brand header is teal with a yellow wordmark, matching .mob-header. */
  --f7-navbar-bg-color:     #007A94;
  --f7-navbar-bg-color-rgb: 0, 122, 148;
  --f7-navbar-text-color:   #FFFFFF;
  /* 56, for a TWO-line identity block (date over familie name).
     Was 62 for three lines including the bismillah; that block measured 43px of
     type with 0px leading between every pair, which is what "merged on top of
     each other" was. The bismillah moved into the Rutiner page, so the bar only
     has to carry 36px of type (18 + 3 + 15) plus 10px clear above and below.

     A subnavbar was tried for the bismillah and abandoned: F7 offsets the page
     by this variable, but the dashboard is pre-rendered with its own 100dvh
     chain and .page-content sits at top 0, so the subnavbar rendered correctly
     and was then painted over by the page.

     Framework7 adds the status-bar inset on top of this by itself, via
     height: calc(var(--f7-navbar-height) + var(--f7-safe-area-top)). Nothing
     here may pin .navbar to a fixed height, or that inset is destroyed and the
     bar slides under the status bar on an installed PWA. */
  --f7-navbar-height:       56px;
  /* The one spacing scale for the app chrome. Every header and tab-bar value
     below comes from this ladder, because the audit found 13 arbitrary values
     (every integer from 1 to 10, plus 16, 20 and 28) across the two chrome
     stylesheets, with no base unit. 4pt base. */
  --fr-space-1:  4px;
  --fr-space-2:  8px;
  --fr-space-3: 12px;
  --fr-space-4: 16px;
  --fr-space-5: 24px;
  /* The project's own minimum comfortable tap target. The header buttons were
     40px against this rule, which is stated three lines below at
     --f7-button-height and was not being honoured by the bar. */
  --fr-tap:     44px;
  --f7-bars-bg-color:       #007A94;
  --f7-bars-bg-color-rgb:   0, 122, 148;
  --f7-bars-text-color:     #FFFFFF;
  --f7-bars-link-color:     #FFD300;      /* --yellow, for tappable chips */
  --f7-bars-border-color:   transparent;

  /* ---- Safe areas --------------------------------------------------- */
  /* THE APP'S OWN safe-area token, read from env() rather than from
     Framework7.

     The head sets viewport-fit=cover, which extends the layout viewport
     underneath the system bars and makes an inset mandatory. The tab-bar rule
     further down asked for it as var(--f7-safe-area-bottom, 0px), on the stated
     assumption that "Framework7 exposes the env() values as --f7-safe-area-*".

     It does not. Enumerated across every stylesheet the app loads, including
     Framework7's own: ZERO declarations of --f7-safe-area-bottom, zero of
     --f7-safe-area-top, and zero rules using env(safe-area-inset-*) anywhere.
     F7's safe-area handling is tied to its ios theme and this app renders on md
     (html carries class "md"). So the fallback won on every device, the inset
     was always 0px, and the bottom of the tab bar sat underneath the Android
     navigation bar — "the bottom edge of the navigation buttons falls outside
     the screen". It cannot reproduce in a desktop browser, where the inset is
     genuinely 0 and the rule is correct by accident.

     Two declarations, not @supports: where env() is understood the second wins,
     and where it is not the declaration is dropped at parse time and the 0px
     stands. Only the BOTTOM is taken. The header is right as it is on the
     device, so nothing here touches the top inset. */
  --fr-safe-bottom: 0px;
  --fr-safe-bottom: env(safe-area-inset-bottom, 0px);

  /* ---- Toolbar (bottom tab bar) ------------------------------------- */
  /* White bar with teal active state reads better for children than a
     dark bar, and keeps the tab labels legible at small sizes.          */
  --f7-toolbar-bg-color:     #FFFFFF;
  --f7-toolbar-bg-color-rgb: 255, 255, 255;
  /* 64, not 56. Framework7 derives the page's bottom padding from this, so a
     toolbar made taller in CSS alone had its labels clipped off the bottom of
     the screen — which is what "the menu buttons are not at the bottom, I have
     to scroll" looked like. Change the variable, not just the rule.

     The inset is added HERE, not only as padding on the bar, for exactly that
     reason: the page's bottom padding has to reserve the taller box too, or the
     content simply scrolls under the extra space. */
  /* ---- The tab bar scales INVERSELY with the viewport ----------------
     Reported 2026-08-04: "the navigation items in the bottom are still very
     small on my s23 but fine on our tablet." Same CSS, same numbers, and both
     observations are correct. A CSS pixel is not a fixed physical size: the
     S23 is 360 CSS px across about 70 mm of glass (~0.19 mm per px) while a
     10-inch tablet is about 800 CSS px across roughly 215 mm (~0.27 mm per
     px). The 10.9px label is therefore ~2.1 mm tall on the phone and ~2.9 mm
     on the tablet — 40 percent larger for free.

     So the scale runs the other way to the usual: the NARROWER the viewport,
     the larger the token. calc(90px - 4vw) gives 75.6px at 360, 73.5px at
     412, 66px at 600, and clamps to the current 64px from about 650 up, so
     the tablet keeps exactly what it has today and only the phone grows.
     Continuous, so there is no breakpoint for a device to fall the wrong side
     of. He accepted that routines scroll sooner in return. */
  --fr-tabbar-h:    clamp(64px, calc(90px - 4vw), 80px);
  --fr-tabbar-icon: clamp(24px, calc(38px - 2.6vw), 30px);
  --fr-tabbar-lbl:  clamp(0.68rem, calc(1.35rem - 2.1vw), 0.84rem);

  --f7-toolbar-height:       calc(var(--fr-tabbar-h) + var(--fr-safe-bottom));
  /* And this is the one that actually governs the tab bar. F7 declares
     `.tabbar-icons { --f7-toolbar-height: var(--f7-tabbar-icons-height) }` on
     the element itself, and an element-level custom property beats an inherited
     :root value whatever the load order. So --f7-toolbar-height above never
     reached the tab bar: the rendered height came from F7's per-theme default,
     which is 80px on Material and 50px on iOS. Measured 80px against a declared
     64px, i.e. 16px of every page sitting under the bar on Android, and a bar
     14px shorter than designed on iPhone. Setting it here makes the declared
     token and the rendered box agree on both platforms. */
  --f7-tabbar-icons-height:  calc(var(--fr-tabbar-h) + var(--fr-safe-bottom));
  --f7-tabbar-link-active-color:   #007A94;
  --f7-tabbar-link-inactive-color: #557C8D;  /* --text-soft, lightened */

  /* ---- Cards -------------------------------------------------------- */
  --f7-card-bg-color:      #FFFFFF;
  --f7-card-text-color:    #1A3040;
  --f7-card-border-radius: 18px;             /* --radius */
  --f7-card-box-shadow:    0 4px 16px rgba(0, 120, 160, 0.12);  /* --shadow */
  --f7-card-margin-horizontal: 12px;
  --f7-card-margin-vertical:   10px;
  --f7-card-header-text-color: #007A94;
  --f7-card-header-font-weight: 800;
  --f7-card-header-border-color: #D8F0F7;    /* --border */
  --f7-card-footer-border-color: #D8F0F7;
  --f7-card-footer-text-color:   #4A7080;    /* --text-soft */

  /* ---- Lists -------------------------------------------------------- */
  --f7-list-bg-color:          #FFFFFF;
  --f7-list-item-border-color: #D8F0F7;

  /* ---- Buttons ------------------------------------------------------ */
  /* 44px is the minimum comfortable tap target (QUALITY_METRICS UX rule);
     children tapping routine cards need generous targets, so the large
     variant goes to 52px.                                               */
  --f7-button-height:            44px;
  --f7-button-large-height:      52px;
  --f7-button-border-radius:     14px;
  --f7-button-font-weight:       800;
  --f7-button-text-transform:    none;
  --f7-button-large-text-transform: none;
  --f7-button-small-text-transform: none;
  --f7-button-fill-bg-color:     #007A94;
  --f7-button-fill-text-color:   #FFFFFF;
  --f7-button-text-color:        #007A94;

  /* ---- Inputs ------------------------------------------------------- */
  --f7-input-height:      46px;
  --f7-input-font-size:   16px;   /* 16px stops iOS Safari zooming on focus */
  --f7-input-text-color:  #1A3040;
  --f7-label-text-color:  #4A7080;

  /* ---- Dialogs, popups, sheets, toasts ------------------------------ */
  --f7-dialog-border-radius:  20px;
  --f7-dialog-button-text-color: #007A94;
  --f7-popup-border-radius:   22px;
  --f7-sheet-border-radius:   22px;
  --f7-sheet-bg-color:        #FFFFFF;
  --f7-toast-bg-color:        rgba(26, 48, 64, 0.92);
  --f7-toast-text-color:      #FFFFFF;
  --f7-toast-border-radius:   14px;

  /* ---- Progress (used by Beloenning goal bars) ---------------------- */
  --f7-progressbar-bg-color:      #E0F7FA;   /* --sky-light */
  --f7-progressbar-progress-color: #008196;  /* --sky */
  --f7-progressbar-height:        8px;
  --f7-progressbar-border-radius: 4px;

  /* ---- Chips and badges --------------------------------------------- */
  --f7-chip-bg-color:   #E0F7FA;
  --f7-chip-text-color: #007A94;
  --f7-chip-height:     30px;
  --f7-badge-bg-color:  #FF6B6B;             /* --coral */
  --f7-badge-text-color: #FFFFFF;

  /* ---- Preloader / skeletons ---------------------------------------- */
  --f7-preloader-color: #008196;
}

/* REMOVED 2026-08-05: a `@media (display-mode: standalone)` block that floored
   --fr-safe-bottom at max(env(safe-area-inset-bottom), 24px).

   It was the second of three attempts at "the bottom edge of the navigation
   buttons falls outside the screen", and it did not work either: reported still
   clipped on the tablet in BOTH orientations with the cache cleared. Both that
   attempt and the one before it assumed the inset could be trusted — first by
   reading env() directly, then by guessing a floor for it when env() answered 0.

   The head no longer sets viewport-fit=cover, so the layout viewport stops
   above the system bars and the OS keeps the bottom edge on screen. env() is
   then legitimately 0 everywhere and --fr-safe-bottom adds nothing, which is
   correct — a floor here would now reserve 24px of white for no reason.
   --fr-safe-bottom is kept, resolving to 0, so the rules that fold it into the
   bar height stay honest if cover is ever reintroduced. */

/* =====================================================================
   Dark mode
   ---------------------------------------------------------------------
   Framework7 scopes dark styling under .dark. The brand hue stays, the
   surfaces invert. Kept deliberately warm rather than pure black so the
   emoji cards do not glare in a bedtime routine.
   ===================================================================== */
.dark {
  --f7-page-bg-color:   #10222B;
  --f7-text-color:      #E6F4F8;
  --f7-block-text-color: #E6F4F8;
  --f7-block-strong-bg-color: #16303B;
  --f7-block-title-text-color: #6FD8EC;
  --f7-card-bg-color:   #16303B;
  --f7-card-text-color: #E6F4F8;
  --f7-card-header-text-color: #6FD8EC;
  --f7-card-header-border-color: #21414F;
  --f7-card-footer-border-color: #21414F;
  --f7-card-footer-text-color:  #9FC4D2;
  --f7-card-box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  --f7-list-bg-color:   #16303B;
  --f7-list-item-border-color: #21414F;
  --f7-toolbar-bg-color: #16303B;
  --f7-toolbar-bg-color-rgb: 22, 48, 59;
  --f7-tabbar-link-inactive-color: #9FC4D2;
  --f7-tabbar-link-active-color:   #6FD8EC;
  --f7-theme-color-text-color: #6FD8EC;
  --f7-input-text-color: #E6F4F8;
  --f7-label-text-color: #9FC4D2;
  --f7-sheet-bg-color:   #16303B;
  --f7-progressbar-bg-color: #21414F;
}

/* =====================================================================
   Structural rules Framework7 variables cannot express
   ===================================================================== */

/* Framework7 ships a bare `button { width: 100% }` rule in its stylesheet.
   That is fine for its own .button components, but this app has around 60
   hand-written <button> elements (period filters, achievement and calendar
   view switchers, calendar arrows, emoji picker tiles, the font-size
   toolbar, routine card edit/delete actions, navbar chips). Under that rule
   every one of them stretched to the full container width and the filter
   rows became vertical stacks.

   `:where()` contributes zero specificity, so this selector weighs exactly
   the same as Framework7's `button` and wins only by load order, while
   still losing to any class-based width (.fc-btn, .w-100, .btn-block).
   Framework7's own .button components are excluded, so its layouts are
   untouched. */
button:where(:not(.button)) { width: auto; }

/* Shiny checkboxes and radios collide with Framework7's own components.
   shiny::checkboxInput() emits
     <div class="checkbox"><label><input type="checkbox"><span>…</span></label></div>
   and Framework7's checkbox component uses that same .checkbox class, hiding
   the native input with display:none because it draws its own icon element.
   Shiny has no such icon, so the box vanished entirely and the label looked
   like plain text: "Seed alderstilpassede rutiner automatisk" was checked by
   default with nothing on screen to say so.

   Native rendering is restored for Shiny-generated inputs only, matched by
   .shiny-input-container so Framework7's own f7Checkbox is untouched. Same
   specificity as Framework7's rule, so this wins on load order. */
.shiny-input-container input[type="checkbox"],
.shiny-input-container input[type="radio"],
.checkbox input[type="checkbox"],
.radio input[type="radio"] {
  display: inline-block;
  width: 20px;
  height: 20px;
  margin: 0 8px 0 0;
  vertical-align: -4px;
  accent-color: var(--sky, #008196);
  -webkit-appearance: checkbox;
  appearance: auto;
  opacity: 1;
  position: static;
  pointer-events: auto;
}
.shiny-input-container input[type="radio"],
.radio input[type="radio"] { -webkit-appearance: radio; }
.checkbox label, .radio label,
.shiny-input-container .checkbox label,
.shiny-input-container .radio label {
  display: flex;
  align-items: center;
  min-height: 44px;      /* touch target */
  cursor: pointer;
  font-weight: 600;
}

/* Focus visibility (RES-04).
   The cards and calendar days are now keyboard-reachable, which is only useful
   if you can see where you are. :focus-visible rather than :focus so a tapping
   child never sees a ring, while a keyboard user always does. The offset keeps
   the ring outside the card's own border and rounded corners. */
.kid-card:focus-visible,
.cal-day:focus-visible,
button:focus-visible,
a:focus-visible,
[role="button"]:focus-visible {
  outline: 3px solid var(--sky-dark, #007A94);
  outline-offset: 2px;
  border-radius: inherit;
}
/* On the teal navbar an ink-coloured ring would disappear, so invert it there. */
.navbar .fr-chip:focus-visible,
.navbar button:focus-visible { outline-color: #FFD300; }

/* selectize (the "Tildelt" picker on Kalender) renders a 34px tall
   .selectize-input as the thing you actually tap, which was the single control
   in the app under the 44px minimum (UX-01). Its inner <input> is a 4x20px
   typing caret and is not a target in its own right; sizing the wrapper is what
   matters. Padding rather than height so a multi-item selection still grows. */
.selectize-input {
  min-height: 44px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
}
.selectize-control.single .selectize-input::after { margin-top: 0; }   /* keep the caret centred */

/* shinyMobile injects its own PWA install prompt as a centred Framework7 toast
   containing #install-button, with hardcoded English "Install" and "Dismiss"
   labels. It appears even with f7Page(allowPWA = FALSE), and it lands right on
   top of the routine cards, which is where a child is trying to tap.
   This app ships its own manifest and service worker, and the browser still
   offers installation through its own menu, so the injected prompt is
   suppressed rather than translated. */
.toast:has(#install-button) { display: none !important; }

/* Framework7 icons default to a 28px font size, which is right for the tab bar
   but oversized next to inline text. Where an icon sits inside a button, a
   chip, a card header or a heading, scale it to the surrounding text. */
.btn i.f7-icons,
.fr-chip i.f7-icons,
.card-header i.f7-icons,
.block-title i.f7-icons,
h1 i.f7-icons, h2 i.f7-icons, h3 i.f7-icons,
h4 i.f7-icons, h5 i.f7-icons, h6 i.f7-icons,
p i.f7-icons, span i.f7-icons, label i.f7-icons {
  font-size: 1.15em;
  width: auto;
  height: auto;
  vertical-align: -0.15em;
}

/* Navbar identity slot.
   The identity block and the lock button are uiOutputs, so they are absent at
   first paint and appear once the server answers, and a layout-shift trace once
   named the header as the largest single contributor to CLS.

   The reservation is on the RIGHT cluster now, not on this slot. That is the
   fix for a whole family of bugs: this rule used to pin the slot to a fixed
   width: 104px with overflow: hidden and justify-content: flex-end, written for
   a right-aligned row of 40px chips that .navbar_chips() no longer produces.
   Consequences, all measured at 412px: the text started at x=39 rather than the
   20px inset and its left edge MOVED with the length of the familie name; 45px
   of content was clipped inside a 44px box; and 122px of the bar, 30% of its
   width, carried nothing at all.

   Reserving .fr-actions instead keeps the geometry identical before and after
   the server answers, while this slot absorbs the slack and truncates inside
   whatever is left. */
.navbar #nav_family_chip_ui {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;          /* lets the flex child actually shrink and ellipsis */
}

/* Framework7's pull-to-refresh preloader is inserted at the top of each
   ptr-content. Pinning it out of flow keeps it from displacing page content
   when Framework7 initialises it. */
.ptr-preloader { position: absolute; }
.navbar #nav_lock_btn_ui {
  display: inline-flex;
  align-items: center;
  min-width: var(--fr-tap);
  min-height: var(--fr-tap);
}

/* Cumulative layout shift.
   The page shell is pre-rendered, but the big uiOutputs arrive from the server
   a moment later. While they are empty they occupy 0px, so everything below
   them (the Præstationer section, the goal card, the day list) is painted high
   up and then jumps down when the content lands. Measured CLS was 0.447,
   against a 0.1 "good" threshold.

   Reserving height while empty removes the jump.

   The reservation on the routine grid is UNCONDITIONAL, not scoped to an empty
   state. Both :empty and :not(:has(*)) proved unreliable: the server renders a
   shimmer skeleton into this container while data loads, so it is never empty,
   and the reservation silently never applied. An unconditional min-height is
   inert once the real cards exceed it, and holds the space in every
   intermediate state, which is what the divider and the Præstationer section
   below it need in order not to be shoved down the page.

   100vh rather than 80vh: the divider sits directly under this container, so
   anything less than a full viewport leaves it on screen during load, where
   its later movement still counts against CLS. */
#routine_columns_ui { min-height: 100vh; }
@supports (height: 100dvh) {
  /* dvh tracks the VISIBLE viewport as Android's URL bar shows and hides, so the
     reservation is exactly one screen rather than one screen-with-the-bar-hidden.
     With 100vh the reservation overshot on Android by the height of the bar. */
  #routine_columns_ui { min-height: 100dvh; }
}

/* ── Parent mode is a CSS state, not a re-render (RX-06) ───────────────────
   The Rutiner grid used to be rebuilt on every parent-mode toggle, because the
   render read parent_mode_rv() to decide which routines to list, whether to
   draw the edit and delete buttons, and which counts to show. That cost 3
   server outputs against a budget of 1 and replaced 10.6 percent of the DOM.

   Now the server renders one markup for both audiences and the difference is
   made here. app.R toggles body.parent-mode over the existing websocket, so the
   whole change is a class flip: 0 outputs, no reactive invalidation.

   This is only safe because the handlers behind these controls check parent
   mode server-side. Rendering a delete button a child cannot see would be
   worthless protection on its own; see req_parent() in R/server_routine.R. */
body:not(.parent-mode) .kid-card[data-sched="0"]       { display: none; }
body:not(.parent-mode) .kid-period-section[data-empty="1"] { display: none; }
body:not(.parent-mode) .rt-card-actions                { display: none; }
body:not(.parent-mode) .rt-add-btn                     { display: none; }
body:not(.parent-mode) .kid-count-parent               { display: none; }
body:not(.parent-mode) .kid-empty-parent               { display: none; }
body.parent-mode       .kid-count-child                { display: none; }
body.parent-mode       .kid-empty-child                { display: none; }
/* The parent card styling rode on a conditional class; it is now always present
   in the class list and gated here instead. */
body:not(.parent-mode) .kid-card.kid-card-parent {
  padding-right: inherit;
}

/* ── Prize locked by behaviour ─────────────────────────────────────────────
   A negative adfærd balance locks the prize however full the routine bar is,
   so the card has to say so. Amber rather than red: this is "not yet", not an
   error, and the child can fix it. Contrast checked: #8A4B00 on #FFF6E5 is
   6.1:1, comfortably past the 4.5 AA requirement. */
.goal-lock-badge {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: 14px;
  background: #FFF6E5;
  border: 2px solid #E0A800;
  color: #8A4B00;
  font-weight: 800;
  font-size: 0.86rem;
  line-height: 1.3;
}
.goal-lock-icon { font-size: 1.15rem; flex: 0 0 auto; }
/* A locked bar still shows how far the routines got, but greyed so it cannot be
   mistaken for a prize that is ready to claim. */
.goal-bar-fill.goal-bar-locked {
  filter: grayscale(0.75);
  opacity: 0.75;
}
.goal-locked-text { color: #8A4B00; font-weight: 800; }

/* ── Popups must outrank the login overlay ─────────────────────────────────
   The login overlay is a fixed layer at z-index 99999 that never leaves the
   DOM: it is hidden by setting opacity 0 and pointer-events none. Framework7
   popups sit at 11000 and their backdrop at 10500, so every dialog in the app
   is painted UNDER a full-screen element, and the only thing saving it is that
   one inline pointer-events:none.
   That is one property away from "I press the lock and nothing happens", which
   is how the PIN dialog was reported from a PC. Any code path that hides the
   overlay without also clearing pointer-events, or any future style that resets
   it, swallows every click on every dialog. Raising the dialogs above the
   overlay removes the dependency entirely rather than trusting it. */
.popup, .popup-backdrop, .sheet-modal, .dialog, .dialog-backdrop,
.actions-modal, .actions-backdrop, .toast, .popover {
  z-index: 100050;
}
.popup-backdrop, .dialog-backdrop, .actions-backdrop { z-index: 100040; }

/* ── The height chain (Android dynamic viewport) ───────────────────────────
   Framework7's layout depends on the root chain being exactly the viewport
   height: .page is position: absolute with bottom: 0, and .page-content does
   the scrolling. Anything that makes html/body taller than the visible area
   moves the tab bar below the fold, because "bottom" then means the bottom of
   something you cannot see.
   That is what happened on a Galaxy S23. islamic_theme.css carried a
   Bootstrap-era body { min-height: 100vh }, and on Android 100vh is the height
   with the URL bar hidden. The bar is usually showing, so body was ~60px
   taller than the screen, the document scrolled, and the menu was unreachable
   without scrolling to the very bottom.
   Locking the chain to 100% and then to 100dvh where supported fixes it, and
   dvh follows the bar as it hides so nothing jumps. */
html, body, .framework7-root { height: 100%; overflow: hidden; }
@supports (height: 100dvh) {
  html, body, .framework7-root { height: 100dvh; }
}
/* The login overlay is a fixed layer outside the F7 tree and still needs to
   cover the screen, so it keeps its own full-height rule (see .fc-wrap). */
#home_routine_ui:not(:has(*))     { min-height: 40vh; }
#cal_grid_ui:not(:has(*))         { min-height: 46vh; }
#reward_goal_card_ui:not(:has(*)) { min-height: 34vh; }
#achievement_ui:not(:has(*))      { min-height: 26vh; }
#cal_day_tasks_ui:not(:has(*))    { min-height: 120px; }

/* A quiet shimmer so the reserved space reads as "loading" rather than "empty".
   Framework7 ships skeleton classes, but these containers are filled by Shiny
   rather than by an F7 component, so the effect is applied directly. */
#routine_columns_ui:not(:has(*)),
#home_routine_ui:not(:has(*)),
#cal_grid_ui:not(:has(*)),
#reward_goal_card_ui:not(:has(*)) {
  background: linear-gradient(100deg,
    rgba(0,174,203,.05) 30%, rgba(0,174,203,.12) 50%, rgba(0,174,203,.05) 70%);
  background-size: 220% 100%;
  border-radius: 18px;
  animation: fr-skeleton 1.3s ease-in-out infinite;
}
@keyframes fr-skeleton {
  0%   { background-position: 120% 0; }
  100% { background-position: -20% 0; }
}
@media (prefers-reduced-motion: reduce) {
  #routine_columns_ui:not(:has(*)), #home_routine_ui:not(:has(*)),
  #cal_grid_ui:not(:has(*)), #reward_goal_card_ui:not(:has(*)) { animation: none; }
}

/* Touch targets.
   WCAG 2.2 SC 2.5.5 (AAA) and Apple's HIG both put the comfortable minimum at
   44x44 CSS px. An audit of the running app found 20 interactive elements that
   met the height but not the width, because they are icon-only buttons sized
   by their glyph: the navbar chips, the achievement day arrows, the calendar
   month arrows, the font-size steppers, and the small card action buttons.
   This app is used by children, so the width is brought up to match. */
.fr-chip,
.achiev-day-nav-btn,
.cal-nav-btn,
.acc-btn,
.acc-reset-btn,
.rt-card-btn {
  min-width: 44px;
  min-height: 44px;
}
.btn-sm { min-height: 44px; min-width: 44px; }

/* The "Ikke din familie? Skift her" link on the login screen is full width but
   only 20px tall. Padding lifts it to a 44px target without moving the text. */
.fc-change-link { padding: 12px 0; }

/* NOTE on contrast (RES-02).
   A first pass at this file "fixed" .card-header and .btn-primary, which a
   contrast audit had reported as white-on-white at a ratio of 1.0. That was
   wrong, and the audit was at fault rather than the app: islamic_theme.css
   gives both a `background: linear-gradient(...)`, which is a background
   IMAGE, so getComputedStyle reports backgroundColor as transparent while the
   gradient still paints. The text is white on teal and perfectly readable.
   The override was reverted. The audit in VERIFICATION_RESULTS.md now resolves
   gradients before judging, and the check is only trusted where the backdrop
   is a flat colour. */

/* Shiny's disconnect overlay (RES-07, RES-08).
   Shiny greys the whole app out and, in most builds, writes "Disconnected from
   server" in English with a Reload link. RES-08 forbids that reaching the user.
   Since the app now reconnects on its own (session$allowReconnect in app.R),
   what the overlay should say is "reconnecting", in the app's own languages,
   without hiding the routines a child was looking at.

   The text is supplied here rather than by replacing Shiny's element, because
   Shiny recreates that element itself and any node we injected would be lost. */
#shiny-disconnected-overlay {
  background: rgba(245, 252, 255, 0.72) !important;
  opacity: 1 !important;
  backdrop-filter: blur(1px);
}
#shiny-disconnected-overlay::after {
  content: "Forbindelsen blev afbrudt. Prøver igen…\A انقطع الاتصال. تتم إعادة المحاولة…";
  white-space: pre-line;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  max-width: 84vw;
  padding: 16px 20px;
  border-radius: 16px;
  background: #007A94;
  color: #fff;
  font-family: 'Nunito', sans-serif;
  font-weight: 800;
  font-size: 0.95rem;
  line-height: 1.55;
  text-align: center;
  box-shadow: 0 8px 32px rgba(0, 120, 160, 0.28);
}
/* Shiny's own "Reload" anchor, if this build renders one. */
#shiny-disconnected-overlay a { display: none !important; }

/* Bilingual error toasts (RES-08). The Danish line leads, the Arabic line sits
   under it in Amiri and reads right to left, matching how routine cards already
   present both languages together. */
.fr-err-da { font-weight: 700; }
.fr-err-ar {
  font-family: 'Amiri', serif;
  font-size: 1.02em;
  opacity: 0.92;
  margin-top: 3px;
}

/* Rows of pill buttons that must scroll sideways rather than wrap. */
.period-filter-bar,
.achiev-view-btns,
.cal-view-btns {
  display: flex;
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.period-filter-bar::-webkit-scrollbar,
.achiev-view-btns::-webkit-scrollbar,
.cal-view-btns::-webkit-scrollbar { display: none; }
.period-filter-btn,
.achiev-view-btn,
.cal-view-btn { flex: 0 0 auto; white-space: nowrap; }

/* Safe-area inset for the tab bar. viewport-fit=cover is set in the head, so
   the bar must clear the Android navigation bar and the iPhone home indicator.
   --fr-safe-bottom is the app's own token, read from env() at the top of this
   file; Framework7 does NOT expose --f7-safe-area-*, which is what this rule
   used to ask for and why the inset was always 0.

   The bar's HEIGHT already includes the inset (--f7-tabbar-icons-height), so
   the padding here reserves the band visually and keeps the white background
   running under the system bar instead of leaving a strip of page showing. */
.toolbar-bottom,
.tabbar {
  padding-bottom: var(--fr-safe-bottom);
}

/* And this is the rule that actually lifts the buttons.
   padding-bottom on .toolbar alone does NOT move them: F7 lays .toolbar-inner
   out as position:absolute, and an absolutely positioned child is placed
   against its container's PADDING box, so it spanned the full 98px and the tab
   links (height:100%) ran to the very bottom edge with them. Measured at a
   34px inset: bar 98px, inner 97.2px, last link bottom 780 = the viewport edge,
   i.e. still underneath the system bar.
   Pinning top and bottom with height:auto puts the inner box in the 64px above
   the inset, which is where the links belong. */
.toolbar-bottom .toolbar-inner,
.tabbar .toolbar-inner {
  top: 0;
  bottom: var(--fr-safe-bottom);
  height: auto;
}

/* The brand wordmark in the navbar: yellow on teal, with the bismillah
   underneath in Amiri, mirroring the old .mob-brand block. */
.navbar .fr-brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.15;
}
/* The gold on the teal navbar is 3.45. Rather than wash the gold out to a
   pale cream to reach 4.5, the title is sized into WCAG's large-text bracket
   (>= 18.66px when bold, i.e. 1.17rem at a 16px root), where the requirement
   is 3.0 and the existing gold passes. The brand colour survives intact. Do
   not drop this below 1.17rem in any media query, or it silently fails again. */
.navbar .fr-brand-name {
  font-size: 1.2rem;
  font-weight: 900;
  color: #FFD300;
  white-space: nowrap;
}
.navbar .fr-brand-bismillah {
  font-family: 'Amiri', serif;
  font-size: 0.65rem;
  color: rgba(255, 255, 255, 0.8);
  direction: rtl;
  white-space: nowrap;
}

/* The navbar is a fixed 48px strip. The chip row must stay on one line
   inside it: the family name is user-supplied and can be long, so it is
   the only element allowed to shrink, and it truncates with an ellipsis
   rather than pushing the other chips out of the bar. */
.navbar .fr-chips {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  height: 100%;
  min-width: 0;
  margin-left: auto;
}
.navbar .fr-chips > * { flex: 0 0 auto; }
/* The family chip is the only flexible element, and it must give up enough
   room that the last chip (font size) is never clipped at the viewport edge.
   5 chips at ~40px plus gaps needs roughly 220px, so on a 414px screen the
   name gets at most about a quarter of the width. */
.navbar .fr-chip-family {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 24vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Small phones (iPhone SE, 320px): drop the family name entirely rather than
   clip the controls, and tighten the remaining chips. The name is still visible
   on the login screen and in Forældre.

   The whole SLOT has to go, not just the name inside it: #nav_family_chip_ui is
   a fixed 104px wrapper, so hiding only .fr-chip-family left the space behind
   and the chip row still ran 58px past the navbar edge at 320px. */
/* CAL-04 requires every calendar day cell to be at least 44x44 at 320, 390 and
   430 px. At 320 the grid sits inside the page padding, so it only gets 305 px:
   7 columns with a 3 px gap gave 41 px cells. Letting the grid span edge to
   edge and tightening the gap yields (320 - 6) / 7 = 44.9 px. The negative
   margins cancel the page-content padding exactly, so nothing overflows. */
@media (max-width: 360px) {
  .cal-month-grid {
    gap: 1px;
    margin-left: -7px;
    margin-right: -7px;
  }
  .cal-day { padding: 4px 1px; }
}

/* REMOVED 2026-07-31: an `@media (max-width: 360px)` block that set
   `.navbar #nav_family_chip_ui { display: none; width: 0 }`.

   It was written to drop a small family-name CHIP on narrow phones, back when
   the header was a wordmark plus five 40px chips. That id now carries the whole
   identity block, so the rule deleted the familie name AND the date on every
   phone 360px wide or narrower, which is the most common Android width there
   is, and is the width of the Galaxy S23 this was reported from. The header
   showed nothing but two buttons on a teal bar.

   It had a second effect that was worse than the missing text. Shiny suspends
   an output whose element is display:none, so #nav_family_chip_ui never
   rendered at all: it sat at class="... recalculating" with empty content and
   did NOT recover when the viewport was widened, because a media-query change
   does not tell Shiny the output became visible.

   The `.fr-chip` and `.fr-brand-*` rules in the same block referenced markup
   that .navbar_chips() stopped emitting in the same rewrite. */

/* Navbar chips (family name, language toggle, lock, font size).
   Kept visually identical to the old .nav-chip so the header reads the
   same after the framework swap. Sized for a comfortable touch target
   while still fitting the 48px bar. */
.navbar .fr-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  /* 44x44 minimum touch target (WCAG 2.2 SC 2.5.5). The visual pill stays
     small via the transparent border trick: the tappable box is 44px, the
     painted background is inset. */
  min-width: 44px;
  height: 44px;
  padding: 0 9px;
  border: none;
  border-radius: 50px;
  /* A white veil over the teal navbar lightened the pill to #298FA5, which
     put its white label at 3.77 and failed AA. Darkening instead of
     lightening keeps the same subtle "raised pill" read and takes the label
     back above 4.5, because the navbar teal itself is already 4.99. */
  background: rgba(0, 0, 0, 0.14);
  color: #FFFFFF;
  font-size: 0.8rem;
  font-weight: 800;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.navbar .fr-chip:active { background: rgba(0, 0, 0, 0.28); }
.navbar .fr-chip-active {
  background: #FFD300;
  color: #1A3040;
}

/* ── The phone was squeezing, and the fix is fewer CARDS, not fewer CHILDREN ──
   Reported 2026-08-04 as "on phone it seems like the app would want to show
   full screen so everything is forced to be zoomed out, which makes the text
   and the icons and everything very small". Nothing was zoomed. The theme puts
   both children side by side down to 320px AND two cards per row inside each
   child, so a 360px phone ended up with a 167px child column and a 72.8px
   routine card, against 174.3px on the tablet.

   MEASURED on the live app, routine card and its label:
     360px   two 167px columns   card 72.8 x 112.8   emoji 33.6px  label 10.7px
     800px   two 383px columns   card 174.3 x 128.8  emoji 41.6px  label 11.7px

   FIRST ATTEMPT, WRONG, kept here because the reasoning matters: one CHILD per
   row below 600px. It gave a 159.5px card, but it also stacked the children, so
   the second child sat about four phone screens down. Confirmed by him: the
   list scrolls, "it moves, but only within the first child". Both children
   being visible at a glance is the point of this screen for a parent.

   So the children stay side by side at every width, and it is the CARDS that go
   to one per row on a phone. Same card size, both children still on screen, and
   the scrolling stays inside each child's own list.

   .kids-columns keeps a column FLOOR rather than a column COUNT. A count switch
   is not monotonic: an earlier version with `1fr` below 600px and the theme's
   `repeat(2, 1fr)` above gave a 600px screen a 273px card and a 601px screen a
   126px one — a wider screen with smaller cards. A 150px floor keeps two
   children side by side from 320px up, and auto-fit collapses the tracks a
   two-child familie does not use, so wider screens simply give both columns
   more room. This overrides the theme's four competing .kids-columns
   declarations; f7_bridge.css loads last, so source order settles it without
   !important.

   140px, not 150px: at 150 a 320px phone could not fit two tracks (2x150 plus
   the 6px gap against ~316px of usable width) and silently fell back to
   stacking, which is the layout this rule exists to avoid. Measured at 320px:
   150px floor stacked the children 2910px apart, 140px keeps them side by
   side. */
.kids-columns {
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}

/* The cards get a floor for the same reason the children do, and it replaced a
   hard `1fr below 700px` that reintroduced the exact cliff this file already
   fixed once: measured, a 700px screen gave a 311.6px card and a 701px screen
   gave a 151.0px one. With a floor the card count follows the column width, so
   a child column shows one card until two of at least 150px genuinely fit.
     360px  column ~167px  1 card   150.6px
     412px  column ~193px  1 card   176.6px
     780px  column ~373px  2 cards  169.3px   (a phone in landscape)
     800px  column ~383px  2 cards  174.3px   (the tablet, unchanged) */
.kid-card-grid {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

@media (max-width: 700px) {

  /* The card grew from 72.8px to 159.5px, but its contents were still sized
     for the cramped one: islamic_theme.css pins them at the <=430px
     breakpoint with `.kid-card-emoji { font-size: 2.1rem !important }` and
     `.kid-card-label { font-size: .67rem !important }` (33.6px and 10.7px
     measured). A 33.6px emoji in a 159.5px card is the same complaint in a
     different place, so these move up to the sizes the tablet already uses in
     a card of comparable width (41.6px and ~11.7px), with a little more on the
     label because 10.7px is about 2 mm of glass on an S23.

     !important is required here, not preferred: the declarations being
     overridden are themselves !important at equal specificity, so nothing
     weaker can win. This is the one case the project's "no !important" habit
     does not cover. */
  .kid-card-emoji { font-size: 2.6rem !important; }
  .kid-card-label { font-size: 0.78rem !important; }
}

/* ── Kom godt i gang, and Om appen ────────────────────────────────────────
   Both are bottom sheets like #fr-more-sheet, but tall and scrollable: the
   guide is six steps and does not fit a phone. They reuse .fr-sheet-grip and
   the same open/closed mechanics so there is one sheet idiom in the app.

   max-height is 88dvh, not vh: on Android 100vh is the URL-bar-hidden height,
   so a vh-sized sheet is taller than the screen and its close button ends up
   under the fold — the same reason the login overlay uses dvh. The bottom
   padding carries --fr-safe-bottom so the last line clears the system bar. */
#fr-guide-backdrop, #fr-about-backdrop {
  position: fixed; inset: 0; z-index: 10500;
  background: rgba(10, 30, 40, .45);
  opacity: 0; pointer-events: none; transition: opacity .22s ease;
}
#fr-guide-backdrop.open, #fr-about-backdrop.open {
  opacity: 1; pointer-events: auto;
}
#fr-guide-sheet, #fr-about-sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10600;
  background: #FFFFFF;
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -10px 40px rgba(0, 0, 0, .28);
  max-height: 88dvh;
  display: flex; flex-direction: column;
  transform: translateY(102%);
  transition: transform .26s cubic-bezier(.22, .8, .3, 1);
}
#fr-guide-sheet.open, #fr-about-sheet.open { transform: translateY(0); }
.fr-guide-scroll {
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 4px var(--fr-space-4) calc(var(--fr-space-4) + var(--fr-safe-bottom));
}
.fr-guide-h {
  font-size: 1.35rem; font-weight: 900; color: #00738A;
  margin: 6px 0 4px;
}
.fr-guide-lead {
  font-size: .95rem; color: #4A7080; margin: 0 0 var(--fr-space-4);
  line-height: 1.5;
}
.fr-guide-step {
  display: flex; gap: var(--fr-space-3);
  padding: var(--fr-space-3) 0;
  border-top: 1px solid #E3F1F5;
}
/* The number is the whole navigation aid: a parent scanning for "where was I"
   finds a digit far faster than a heading. 34px keeps it legible at the app's
   largest text setting without wrapping the title beside it. */
.fr-guide-num {
  flex: 0 0 34px; height: 34px; border-radius: 50%;
  background: #00738A; color: #FFFFFF;
  font-weight: 900; font-size: 1rem;
  display: flex; align-items: center; justify-content: center;
}
.fr-guide-step-title {
  font-weight: 800; font-size: 1rem; color: #1A3040; margin-bottom: 2px;
}
.fr-guide-step-body {
  font-size: .92rem; line-height: 1.55; color: #40606E;
}
.fr-guide-h3 {
  font-size: 1rem; font-weight: 800; color: #00738A;
  margin: var(--fr-space-4) 0 var(--fr-space-2);
}
.fr-guide-tips { margin: 0; padding-left: 1.1rem; }
.fr-guide-tips li {
  font-size: .92rem; line-height: 1.55; color: #40606E; margin-bottom: 6px;
}
[dir="rtl"] .fr-guide-tips { padding-left: 0; padding-right: 1.1rem; }
.fr-guide-close {
  width: 100%; min-height: var(--fr-tap);
  margin: var(--fr-space-4) 0 var(--fr-space-2);
  border: none; border-radius: 50px;
  background: #00738A; color: #FFFFFF;
  font-size: 1rem; font-weight: 800; cursor: pointer;
}
.fr-about-line { font-size: 1rem; color: #1A3040; margin: 4px 0 10px; }
.fr-about-ver  { font-size: .85rem; color: #5E7B88; font-weight: 700; }

/* "Hvad er nyt" in the parent panel. */
.fr-whatsnew-rel { padding: var(--fr-space-2) 0; }
.fr-whatsnew-rel + .fr-whatsnew-rel { border-top: 1px solid #E3F1F5; }
.fr-whatsnew-head {
  display: flex; align-items: baseline; gap: var(--fr-space-2);
  flex-wrap: wrap; margin-bottom: 4px;
}
.fr-whatsnew-ver  { font-weight: 800; color: #00738A; font-size: .95rem; }
.fr-whatsnew-date { font-size: .78rem; color: #7A96A3; }
.fr-whatsnew-list { margin: 0; padding-left: 1.1rem; }
.fr-whatsnew-list li {
  font-size: .9rem; line-height: 1.5; color: #40606E; margin-bottom: 4px;
}
[dir="rtl"] .fr-whatsnew-list { padding-left: 0; padding-right: 1.1rem; }

/* The recovery code, shown once. Monospace and wide tracking because it is
   meant to be copied onto paper and read back weeks later; the alphabet
   already drops O/0 and I/1 for the same reason (see R/access.R). */
.fr-recovery-code {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 1.6rem; font-weight: 800; letter-spacing: .18em;
  text-align: center; color: #00738A;
  background: #E8F7FA; border: 2px dashed #4FB3C7; border-radius: 14px;
  padding: var(--fr-space-4) var(--fr-space-2);
  margin: var(--fr-space-3) 0;
  user-select: all;              /* one tap selects the whole code */
}
/* "Glemt PIN?" is a way out of a dead end, not a primary action, so it reads
   as a link. Still a full tap target: it is reached by someone already stuck. */
.fr-linkish {
  display: block; width: 100%; min-height: var(--fr-tap);
  margin-top: var(--fr-space-2);
  background: none; border: none;
  color: #00738A; font-size: .92rem; font-weight: 700;
  text-decoration: underline; cursor: pointer;
}

/* A period chip being carried. pointer-events:none is not decoration: without
   it elementFromPoint returns the chip under the finger, which IS this one, and
   the drop target can never be worked out. The lift and shadow say "picked up";
   the cursor is for the desktop case. */
.period-filter-btn.fr-chip-dragging {
  pointer-events: none;
  transform: scale(1.06) translateY(-2px);
  box-shadow: 0 8px 22px rgba(0, 0, 0, .28);
  opacity: .92;
  cursor: grabbing;
  z-index: 5;
}
/* Only a parent can rearrange them, so only a parent gets the affordance. */
body.parent-mode .period-filter-btn { cursor: grab; }

/* Tab bar labels: Danish words like "Belønning" are long, so allow the
   label to shrink rather than wrap or clip. */
.tabbar .tab-link span.tabbar-label,
.tabbar .tab-link .tabbar-label {
  font-size: var(--fr-tabbar-lbl);
  font-weight: 800;
  letter-spacing: -0.01em;
  white-space: nowrap;
}

/* The tab glyph, sized from the same token as the bar.
   The obvious route does nothing: setting --f7-tabbar-icon-size, on :root or on
   the .tabbar element itself, left the glyph at 25px. Verified in the running
   app by setting it to 31px live — nothing moved. This build does not size the
   tab glyph from that variable, unlike --f7-tabbar-icons-height, which it does
   (see the note on that token above). Do not assume an --f7-* name is wired up
   because a neighbouring one is.
   The selector has to be THIS specific because of a rule 430 lines below in
   this same file: `.toolbar .tab-link i.f7-icons { font-size: 25px }`, which is
   also (0,3,1). An equally specific rule loses on source order, so a plain
   `.tabbar .tab-link i.icon` would be silently overridden. Setting width and
   height is pointless: the glyph is an inline <i>, so only font-size governs
   its box. */
.tabbar.tabbar-icons .tab-link i.icon.f7-icons,
.tabbar.tabbar-icons .tab-link i.icon.material-icons {
  font-size: var(--fr-tabbar-icon);
  line-height: 1;
}

/* Right-to-left: Framework7 handles layout direction, but the Arabic face
   and the slightly larger optical size are ours. */
[dir="rtl"] .f7-ar,
.ar-text {
  font-family: 'Amiri', serif;
  font-size: 1.08em;
  line-height: 1.9;
}

/* =====================================================================
   Login / family-code screen
   ---------------------------------------------------------------------
   This screen sits OUTSIDE the Framework7 component tree (it is a fixed
   overlay above the pre-rendered dashboard), so it cannot borrow F7
   component styles. It used Bootstrap classes before. Those rules are
   reproduced here so removing Bootstrap does not change how it looks.
   ===================================================================== */
.fc-wrap {
  /* dvh for the same reason as the height chain above: on Android 100vh is the
     URL-bar-hidden height, which made this overlay a little taller than the
     screen and gave it a pointless scroll. The overlay itself is position:fixed
     with overflow-y:auto, so a shrunken viewport (keyboard up) can still reach
     the login button. */
  min-height: 100vh;
  min-height: 100dvh;   /* overrides the line above where dvh is supported */
  display: flex;
  align-items: center;
  justify-content: center;
  background: #F5FCFF;
  font-family: 'Nunito', sans-serif;
  padding: 24px 16px;
}
.fc-card {
  max-width: 460px;
  width: 100%;
  margin: 0 auto;
  padding: 40px 36px;
  background: #FFFFFF;
  border-radius: 28px;
  box-shadow: 0 12px 48px rgba(0, 120, 160, 0.18);
  border-top: 6px solid #008196;
}
.wb-bar-track {
  height: 6px;
  border-radius: 4px;
  background: #E0F7FA;
  overflow: hidden;
  margin: 14px 0 20px;
}
.wb-bar-fill {
  height: 100%;
  border-radius: 4px;
  background: #008196;
  width: 0%;
  transition: width 2.6s linear;
}
.wb-code-chip {
  display: inline-block;
  background: #E0F7FA;
  color: #007A94;
  font-weight: 900;
  font-size: 1.15rem;
  padding: 10px 28px;
  border-radius: 50px;
  border: 2.5px solid #008196;
  letter-spacing: 0.05em;
  margin-bottom: 4px;
  word-break: break-all;
  max-width: 100%;
}
.fc-features {
  background: #E8F5E9;
  border-radius: 14px;
  padding: 14px 18px;
  margin-bottom: 20px;
  font-size: 0.88rem;
}
.fc-features ul {
  margin: 6px 0 0;
  padding-left: 18px;
  color: #4A7080;
  line-height: 2;
}
.fc-hr { border: 0; border-top: 1px solid #D8F0F7; margin: 16px 0; }
.fc-change-link {
  font-size: 0.83rem;
  color: #557C8D;
  text-decoration: none;
  cursor: pointer;
  display: block;
  text-align: center;
  margin-top: 10px;
}
.fc-change-link:hover { color: #007A94; text-decoration: underline; }

/* The code field and submit button, previously .form-control-lg and
   .btn.btn-primary.btn-lg. 16px font size keeps iOS Safari from zooming
   the page when the field takes focus. */
.fc-input {
  width: 100%;
  display: block;
  text-align: center;
  font-family: inherit;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  border-radius: 16px;
  border: 2.5px solid #D8F0F7;
  padding: 14px 20px;
  color: #1A3040;
  background: #FFFFFF;
  outline: none;
  -webkit-appearance: none;
}
.fc-input:focus { border-color: #008196; }
.fc-btn {
  width: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 52px;
  padding: 14px 20px;
  border: none;
  border-radius: 16px;
  font-family: inherit;
  font-weight: 800;
  font-size: 1.05rem;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.fc-btn-primary { background: #008196; color: #FFFFFF; }
.fc-btn-primary:active { background: #007A94; }
.fc-btn:disabled { opacity: 0.65; cursor: default; }
.fc-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #FFFFFF;
  border-radius: 50%;
  display: inline-block;
  animation: fc-spin 0.7s linear infinite;
}
@keyframes fc-spin { to { transform: rotate(360deg); } }

/* Small utility classes the old markup inherited from Bootstrap and that
   are still used by the login screen and the accessibility toolbar. */
.text-center { text-align: center; }
.mt-1 { margin-top: 0.25rem; }

/* Respect a user's reduced-motion preference: Framework7 animates page
   and tab transitions by default. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --f7-page-transition-duration: 0ms;
    --f7-page-swipeback-transition-duration: 0ms;
  }
  .tab, .page, .card { transition: none !important; animation: none !important; }
  /* UX-14 names three things specifically: confetti, the card flip animation
     and the tab slide. The first is JavaScript and is suppressed in
     animations.js; the tab slide is covered above; the card flip is this. The
     done state itself is untouched, so completion is still communicated, just
     without the bounce. */
  .kid-card.just-done { animation: none !important; }
  .period-just-done   { animation: none !important; }
  /* Anything else that animates purely for delight. */
  .celebration-emoji, #confetti-canvas { display: none !important; }
}

/* A task drawn optimistically, before the server has confirmed it (CAL-03).
   Muted and with an hourglass so it reads as "on its way" rather than done.
   It is replaced by the real row when cal_day_tasks_ui re-renders, or removed
   by fr_task_add_failed if the write failed. */
.task-item.task-pending { opacity: 0.72; }

/* ── Loading placeholder for the routine grid (RUT-08) ─────────────────────
   These rules live here, not inside the renderUI that used to carry them,
   because the placeholder is now part of the pre-rendered uiOutput container
   and is on screen before any server output exists. Styles delivered by a
   render cannot style something that renders before it.

   Geometry is inherited: the placeholder cards carry the real .kid-card class
   inside a real .kid-card-grid, so they match the live layout at every
   breakpoint instead of at the one that happened to be measured. Only the
   paint is overridden. */
@keyframes rt-shimmer {
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
.skeleton-shimmer {
  background: linear-gradient(90deg, #e0f0f8 25%, #f0faff 50%, #e0f0f8 75%);
  background-size: 200% 100%;
  animation: rt-shimmer 1.4s infinite;
}
.skeleton-colhead {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px 10px; background: #fff;
}
.skeleton-name { width: 84px; height: 22px; border-radius: 8px; }
.skeleton-bar  { flex: 1; height: 9px; border-radius: 50px; }
.skeleton-periodhead { height: 34px; }
.skeleton-card {
  border-color: #eaf6fb !important;
  cursor: default !important;
}
.skeleton-card > * { visibility: hidden; }
/* UX-14: a placeholder is decoration, so it must hold still when motion is
   reduced. The shape stays, only the shimmer stops. */
@media (prefers-reduced-motion: reduce) {
  .skeleton-shimmer { animation: none; }
}

/* ============================================================================
   APP CHROME, 2026-07-31
   ----------------------------------------------------------------------------
   Reported: "it doesn't feel like an app, more like a home page", "the main
   menu navigation buttons are really really small", and the familie name
   rendering as "testFamil".

   The cause was a header doing a website's job: an app wordmark plus a bismillah
   plus five 40px chips in a 48px bar, which left the familie name 24vw. The bar
   now carries identity on the left and one labelled action on the right;
   everything else moved to a sheet with 56px rows.

   The boldness is spent in ONE place: the tab bar's active lozenge. Everything
   else here is quiet on purpose.
   ========================================================================== */

/* ── Header ───────────────────────────────────────────────────────────────── */
/* One symmetric inset on both sides. It used to be 20px on the left (from a
   Bootstrap-era `.navbar { padding: 0 20px !important }`) and 4px on the right,
   which is below any thumb-safe edge margin. */
.navbar .navbar-inner {
  padding-left:  var(--fr-space-4);
  padding-right: var(--fr-space-4);
}
/* f7Navbar(title = NULL) still emits an empty `.title` div, and Framework7
   gives it `margin-left: 16px`. An empty element with a margin is invisible but
   not free: it pushed the identity block to x=36 instead of the 16px inset.
   Framework7 also puts `margin: 0 4px` on `.left` and `.right` via
   `.md .navbar .left`, which is where the remaining 4px came from.
   Measured before: left inset 36px, right inset 20px. After: 16px both. */
.navbar .navbar-inner > .title:empty { display: none; }
.navbar .navbar-inner > .left,
.navbar .navbar-inner > .right { margin-left: 0; margin-right: 0; }
.navbar .fr-ident { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }

/* Two lines, 3px apart, both solid white.
   The old block set line-height 1.15 on the container and margin-top 0 on the
   date (in a second, later declaration that silently overrode a margin-top of
   1px 155 lines earlier), so all three line boxes were exactly contiguous.
   Hierarchy here is carried by SIZE and ORDER only. Opacity is what put the
   date at 3.36:1 and the bismillah at 4.10:1 on this teal, both below the 4.5:1
   floor, so neither line uses it. */
.fr-ident-block {
  display: flex; flex-direction: column; justify-content: center;
  min-width: 0; gap: 3px;
}
/* The familie name leads and the date is its subtitle (2026-08-04). */
.fr-ident-name {
  font-size: .9375rem;            /* 15px — the title */
  font-weight: 700; line-height: 1.2; color: #FFFFFF;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  letter-spacing: .1px;
}
/* The date is the subtitle. Both lines stay solid #FFFFFF: the earlier version
   faded the second line to rgba(255,255,255,.72), which measured 3.36:1 on this
   teal and fails WCAG 1.4.3. Size and weight carry the hierarchy instead. */
.fr-ident-date {
  font-size: .75rem;              /* 12px — the subtitle */
  font-weight: 600; line-height: 1.25; color: #FFFFFF;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Danish and Arabic dates both ship; show the one matching the direction. */
.fr-date-ar { display: none; }
[dir="rtl"] .fr-date-da { display: none; }
[dir="rtl"] .fr-date-ar { display: inline; }
/* The familie name is Latin script even when the interface is Arabic, so it
   keeps its own direction and clips at the end rather than the start. In RTL a
   truncating name used to render "stFamilie" with no ellipsis to show the cut. */
[dir="rtl"] .fr-ident-name { direction: ltr; text-align: right; }

/* The right cluster carries the CLS reservation: 44 + 8 + 44 at the narrowest,
   growing to fit the pill's label. Reserving here rather than on the identity
   slot is what lets the left column be fluid. */
.navbar .fr-actions {
  display: flex; align-items: center; gap: var(--fr-space-2);
  flex: 0 0 auto; margin-left: auto;
}

/* The primary action. Labelled, because an unlabelled padlock in a row of
   icons is not a way in that anyone finds. 44px, not 40: the project's own
   minimum tap target, which the header was not honouring. */
.fr-lock-btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px;
  min-height: var(--fr-tap); padding: 0 var(--fr-space-3);
  border: 1.5px solid rgba(255,255,255,.55);
  border-radius: 999px;
  background: rgba(255,255,255,.12);
  color: #FFFFFF; font-weight: 700; font-size: .8125rem;   /* 13px */
  cursor: pointer; white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, transform .1s ease;
}
.fr-lock-btn:active { transform: scale(.96); }
.fr-lock-btn i.f7-icons { font-size: 18px; }
.fr-lock-btn.is-open {
  background: #FFD300; border-color: #FFD300; color: #2A2000;
}
.fr-icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--fr-tap); min-height: var(--fr-tap); padding: 0;
  border: none; border-radius: 12px;
  background: rgba(255,255,255,.12); color: #FFFFFF; cursor: pointer;
  transition: background .15s ease, transform .1s ease;
}
.fr-icon-btn:active { transform: scale(.94); }
.fr-icon-btn i.f7-icons { font-size: 19px; }
.fr-lock-btn:focus-visible, .fr-icon-btn:focus-visible {
  outline: 3px solid #FFD300; outline-offset: 2px;
}
/* ONE structural breakpoint, not five.
   The two identity lines stack vertically, so horizontal width never decides
   whether they fit: the block is 36px tall at 320px and at 430px alike, and
   each line truncates with an ellipsis inside whatever width is left. An
   earlier version of this fix hid the familie name below 390px and measuring
   showed that was wrong — at 360px there were still 48px of unused slack.
   So both lines show at every width, and the DATE never disappears. It used to
   be the first thing dropped, which left a parent on a 360px Android looking at
   two buttons on an empty bar.

   The only thing that genuinely runs out of horizontal room is the pill's
   label, competing with a 44px icon button on the same row. */
@media (max-width: 359.98px) {
  .fr-lock-txt { display: none; }
  .fr-lock-btn { padding: 0; min-width: var(--fr-tap); }
}

/* REMOVED 2026-07-31: `.navbar .subnavbar`, `.subnavbar-inner` and
   `.fr-bismillah-strip`. Nothing has emitted a subnavbar since the header
   rewrite (see the note at f7Navbar in ui_dashboard.R explaining why a
   subnavbar cannot work here: the pre-rendered 100dvh dashboard paints over
   it). The bismillah now opens the Rutiner page instead. */

/* ── The bismillah ────────────────────────────────────────────────────────── */
/* Out of the 56px header and into the page, at a size where it can actually be
   read. In the bar it was 10.9px Amiri at 4.10:1 on teal: below the size at
   which harakat resolve, failing contrast, clipped top and bottom by a
   line-height smaller than the font's own glyph extent, and one longer familie
   name away from silently truncating to "...الرحمن الرحيم".
   16px, full width, its own space, no truncation possible. */
.fr-bismillah-open {
  display: block; width: auto;
  padding-inline: var(--fr-space-4);
  margin: 2px 0 10px;
  font-family: 'Amiri', 'Scheherazade New', serif;
  font-size: 1rem; line-height: 1.9;
  color: #00738A;
  direction: rtl; text-align: center;
  letter-spacing: .2px;
}
@media (prefers-reduced-motion: no-preference) {
  .fr-bismillah-open { transition: opacity .2s ease; }
}

/* ── The overflow sheet ───────────────────────────────────────────────────── */
#fr-more-backdrop {
  position: fixed; inset: 0; background: rgba(6,32,40,.45);
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
  z-index: 100040;
}
#fr-more-backdrop.open { opacity: 1; pointer-events: auto; }
#fr-more-sheet {
  position: fixed; left: 0; right: 0; bottom: 0;
  background: #FFFFFF; border-radius: 20px 20px 0 0;
  /* --fr-safe-bottom, not --f7-safe-area-bottom: Framework7 never declares the
     latter (see the token at the top of this file), so this sheet's last row —
     "Skift familie" — sat under the Android navigation bar exactly like the tab
     bar did. This one is position:fixed with its own padding, so the padding is
     the right lever here. */
  padding: 8px 12px calc(12px + var(--fr-safe-bottom));
  transform: translateY(102%); transition: transform .22s cubic-bezier(.32,.72,0,1);
  z-index: 100045; box-shadow: 0 -8px 32px rgba(0,0,0,.18);
}
#fr-more-sheet.open { transform: translateY(0); }
.fr-sheet-grip {
  width: 40px; height: 4px; border-radius: 999px; background: #CBD9E0;
  margin: 4px auto 10px;
}
.fr-sheet-row {
  display: flex; align-items: center; gap: 14px; width: 100%;
  min-height: 56px; padding: 8px 10px; margin-bottom: 2px;
  border: none; background: transparent; border-radius: 14px;
  text-align: left; cursor: pointer; color: var(--text, #1A3040);
}
.fr-sheet-row:active { background: #EAF6F9; }
.fr-sheet-row:focus-visible { outline: 3px solid #00838F; outline-offset: -2px; }
.fr-sheet-ic {
  display: inline-flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; flex: 0 0 40px;
  border-radius: 12px; background: #E3F5F8; color: #00738A;
  font-size: 1.05rem; font-weight: 800;
}
.fr-sheet-txt { display: flex; flex-direction: column; min-width: 0; }
.fr-sheet-title { font-weight: 800; font-size: .95rem; }
.fr-sheet-sub   { font-size: .78rem; color: #6B8794; margin-top: 1px; }
.fr-sheet-cancel { justify-content: center; margin-top: 4px; background: #F1F6F8; }
.fr-sheet-cancel .fr-sheet-title { color: #6B8794; }
[dir="rtl"] .fr-sheet-row { text-align: right; }

/* ── Tab bar ──────────────────────────────────────────────────────────────── */
/* "The main menu navigation buttons are really really small." They were 24px
   glyphs adrift in an 80px bar. The icon grows, the label grows, and the active
   tab gets a filled lozenge so which tab you are on is readable at a glance. */
.toolbar-bottom, .tabbar {
  /* Height comes from --f7-toolbar-height above so F7's page offset agrees. */
  background: #FFFFFF !important;
  border-top: 1px solid #DCE9EE;
  box-shadow: 0 -2px 14px rgba(10,60,75,.07);
}
/* display WITHOUT !important, deliberately. frSetTabLink() hides the gated
   Forældre and System tabs with an inline style="display:none", and an
   !important here beat that inline style and put both parent-only tabs in
   front of every child. The selector is already specific enough (0,2,0) to
   win against Framework7's own .tab-link rule without it. */
.toolbar-bottom .tab-link, .tabbar .tab-link, .toolbar .tab-link {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  height: 100%; padding: 5px 2px 4px;
  color: #6B8794 !important; opacity: 1 !important;
  font-size: .70rem; font-weight: 700; letter-spacing: .1px;
  -webkit-tap-highlight-color: transparent;
}
.toolbar .tab-link i.f7-icons {
  font-size: 25px; line-height: 1;
  padding: 3px 16px; border-radius: 12px;
  transition: background .16s ease, color .16s ease, transform .16s ease;
}
.toolbar .tab-link.tab-link-active { color: #00738A !important; }
.toolbar .tab-link.tab-link-active i.f7-icons {
  background: #D5F0F6; color: #00738A; transform: translateY(-1px);
}
.toolbar .tab-link:active i.f7-icons { transform: scale(.9); }
.toolbar .tab-link:focus-visible { outline: 3px solid #00838F; outline-offset: -3px; }
@media (prefers-reduced-motion: reduce) {
  .toolbar .tab-link i.f7-icons { transition: none; }
  #fr-more-sheet, #fr-more-backdrop { transition: none; }
}

/* REMOVED 2026-07-31: a SECOND set of .fr-ident-* declarations that lived here,
   180 lines after the first set, and silently won on source order:

     .fr-ident-name { font-size: 1.0rem; line-height: 1.15; }
     .fr-ident-date { font-size: .62rem; margin-top: 0; }
     .fr-ident-bismillah { ... }
     @media (max-width: 340px) { .fr-ident-date { display: none; } }

   This is why the header could not be fixed by editing the block above: the
   date's `margin-top: 0` here overrode a `margin-top: 1px` declared earlier,
   which is how all three line boxes ended up exactly contiguous, and the
   font-sizes here overrode the intended ones. Verified after deleting: the date
   renders at 15px and the name at 12px, as the block above declares.

   The 340px query was unreachable anyway — the 360px rule already removed the
   whole slot before it could apply. */
