/* ═══ 5. FEATURES (Announcements · Enrichments · HR · TA · Messenger · Broadcast) ═══ */

/* Announcements */
.announcement-card {
    padding: 16px;
    background: var(--color-bg);
    border-radius: 8px;
    margin-bottom: 12px;
    border-left: 3px solid var(--color-primary);
}

.announcement-card.unread {
    border-left-color: var(--color-accent);
    background: #FFFBF0;
}

.announcement-card .announcement-title {
    font-weight: 600;
    margin-bottom: 4px;
}

.announcement-card .announcement-date {
    font-size: var(--font-size-small);
    color: var(--color-text-light);
    margin-bottom: 8px;
}

/* Clickable author name in the announcement byline (card list + detail modal).
   Uses the brand primary, but in dark mode an org with no custom dark primary
   leaves --color-primary as its dark light-mode brand colour (fine as a button
   background, too dark as text on the dark card), so fall to the legible
   --color-primary-light in dark mode - same approach as .ctx-alert-info. */
.ann-author {
    cursor: pointer;
    color: var(--color-primary);
}
[data-theme="dark"] .ann-author {
    color: var(--color-primary-light);
}

.announcement-card .announcement-body {
    font-size: var(--font-size-body);
    line-height: 1.5;
}

.announcement-card .announcement-preview {
    font-size: var(--font-size-body);
    color: var(--color-text-light);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-top: 4px;
    line-height: 1.4;
}

.announcement-card .announcement-thumbs {
    display: flex;
    gap: 6px;
    margin-top: 8px;
    flex-wrap: wrap;
}

.announcement-card .announcement-thumbs img {
    width: 60px;
    height: 60px;
    object-fit: cover;
    border-radius: 6px;
}

.announcement-card.clickable {
    cursor: pointer;
    transition: background 0.15s;
}

.announcement-card.clickable:hover {
    background: rgba(232, 105, 30, 0.04);
}

/* Announcement detail modal */
.ann-detail-body {
    font-size: var(--font-size-body);
    line-height: 1.7;
    white-space: pre-wrap;
    margin: 16px 0;
}

.ann-detail-images {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 12px;
}

.ann-detail-images img {
    width: 100px;
    height: 100px;
    object-fit: cover;
    border-radius: 8px;
    cursor: pointer;
    transition: opacity 0.15s;
}

.ann-detail-images img:hover {
    opacity: 0.8;
}

.ann-detail-image-wrap {
    position: relative;
    display: inline-block;
}

.ann-detail-image-wrap img {
    display: block;
}

.ann-detail-image-delete {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    border: none;
    font-size: 30px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}

.ann-detail-title {
    font-size: var(--font-size-heading);
    font-weight: 700;
    margin-bottom: 4px;
}

.ann-detail-byline {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    margin-bottom: 16px;
}

/* ── Announcement translation control (mirrors .msgr-translation in
   style-07.css: sparkles glyph + muted text, same conjure-reveal animation).
   Rendered by js/admin-announcements-translate.js right after the body, ABOVE
   the images/admin-controls section, so both staff and parent detail-modal
   readers see it. Unlike the messenger's inline-below-bubble treatment this
   is a full-width action row (buttons before a translation exists, a result
   row + "Show original" toggle after). */
.ann-tx-area { margin: 4px 0 16px; }
.ann-tx-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.ann-tx-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: var(--color-bg);
    color: var(--color-primary);
    font-size: var(--font-size-label);
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}
.ann-tx-btn:hover { background: rgba(232, 105, 30, 0.06); border-color: var(--color-primary); }
.ann-tx-btn .fi { font-size: 0.95em; }
.ann-tx-result {
    display: flex;
    gap: 6px;
    align-items: baseline;
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    margin-top: 6px;
}
.ann-tx-glyph { color: var(--color-primary); font-size: 0.95em; flex-shrink: 0; transform: translateY(1px); }
.ann-tx-loading { animation: msgrTxPulse 1.1s ease-in-out infinite; }
.ann-tx-error a { color: var(--color-primary); cursor: pointer; }
.ann-tx-show-original { color: var(--color-primary); cursor: pointer; text-decoration: underline; }
/* Same "conjure" reveal as the messenger's inline translation, played once
   when the translated title/body first paint. */
.ann-tx-reveal #ann-title-display,
.ann-tx-reveal #ann-body-display {
    animation: msgrTxConjure var(--dur-celebrate) var(--ease-out) both;
    will-change: opacity, transform, filter;
}
.ann-tx-reveal .ann-tx-glyph {
    transform-origin: center;
    animation: msgrTxSparkle var(--dur-celebrate) var(--ease-spring) both;
}

.ann-detail-meta {
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--color-border);
}

.ann-detail-meta-row {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 12px;
    padding: 6px 0;
    font-size: var(--font-size-body);
}

.ann-detail-meta-label {
    font-weight: 600;
    color: var(--color-text-light);
    flex-shrink: 0;
}

.ann-detail-delete {
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--color-border);
    text-align: center;
}

.ann-reads-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--color-border);
    background: var(--color-bg-subtle, #f5f5f5);
    font-size: var(--font-size-label);
    color: var(--color-text);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
    user-select: none;
}
.ann-reads-chip:hover {
    background: rgba(232, 105, 30, 0.1);
    border-color: var(--color-primary);
    color: var(--color-primary);
}
[data-theme="dark"] .ann-reads-chip {
    background: #2a2a2a;
    border-color: var(--color-border);
    color: var(--color-text);
}

/* Announcement image upload previews */
.ann-image-preview {
    display: inline-block;
    position: relative;
    margin: 4px;
}

.ann-image-thumb {
    width: 60px;
    height: 60px;
    object-fit: cover;
    border-radius: 6px;
}

.ann-image-remove {
    position: absolute;
    top: -8px;
    right: -8px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--color-error);
    color: #fff;
    border: none;
    font-size: 18px;
    line-height: 28px;
    text-align: center;
    cursor: pointer;
    padding: 0;
}

.enr-image-thumb {
    width: 80px;
    height: 80px;
    cursor: pointer;
}

/* Image lightbox */
.image-lightbox {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.9);
    z-index: 99999 !important;
    align-items: center;
    justify-content: center;
    cursor: pointer;
}

.image-lightbox.show {
    display: flex;
    /* MA6 — open: backdrop fades in (staging) while the photo rises from a
       slightly smaller scale and settles (follow-through). The animation plays
       the instant .show flips display:none -> flex. */
    animation: lightboxBackdropIn 200ms var(--ease-out);
}
.image-lightbox.show img {
    animation: lightboxImgIn 260ms var(--ease-spring);
}
/* Shared-element FLIP open: image-crop.js drives the backdrop's background-color
   inline so it fades in over the SAME duration + easing as the grow (and only the
   backdrop dims — the photo grows without also fading). Suppress the CSS opacity
   keyframe so the two don't compound. Must !important-beat .show's animation. */
.image-lightbox.lb-bd-js { animation: none !important; }
/* MA6 — close: JS adds .lb-closing, the reverse animation plays, then
   closeLightbox() removes .show after a setTimeout (image-crop.js). These
   rules must follow .show in source order so the close animation wins. */
.image-lightbox.lb-closing {
    animation: lightboxBackdropOut 160ms var(--ease-out) forwards;
}
.image-lightbox.lb-closing img {
    animation: lightboxImgOut 160ms var(--ease-out) forwards;
}
@keyframes lightboxBackdropIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}
@keyframes lightboxBackdropOut {
    from { opacity: 1; }
    to   { opacity: 0; }
}
/* Resting transform is translate(0,0) scale(1) (see .image-lightbox img); the
   keyframes land exactly there so the photo hands back to its zoom/pan transform
   without a jump once the open animation ends. */
@keyframes lightboxImgIn {
    from { transform: translate(0,0) scale(0.92); }
    to   { transform: translate(0,0) scale(1); }
}
@keyframes lightboxImgOut {
    from { transform: translate(0,0) scale(1); }
    to   { transform: translate(0,0) scale(0.96); }
}

.image-lightbox img {
    max-width: 90vw;
    max-height: 90vh;
    object-fit: contain;
    border-radius: 8px;
    transform-origin: center center;
    transform: translate(0,0) scale(1);
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
    -webkit-user-drag: none;
    /* Suppress iOS's native long-press callout — its "Save to Photos" silently
       fails for this element (the -webkit-user-* + will-change styling breaks
       WKWebView image saving). The lightbox Save button replaces it. */
    -webkit-touch-callout: none;
    cursor: zoom-in;
    will-change: transform;
}
.image-lightbox img.lb-zoomed    { cursor: zoom-out; }
.image-lightbox img.lb-grabbing  { cursor: grabbing; }
.image-lightbox img.lb-animating { transition: transform 0.2s ease-out; }

/* Shared-element FLIP (expand-from-origin / shrink-back, driven by image-crop.js;
   the "expand-origin" motion primitive). While the photo flies to/from its origin
   thumbnail, suppress the centre-scale open/close keyframes so the JS-driven
   inline transform owns the geometry. */
.image-lightbox img.lb-flip { animation: none !important; }
/* On the close FLIP the chrome fades out of the way (the backdrop dims via an
   inline transition) so only the shrinking photo and the revealed page remain.
   Reuses the .lb-dragging transition on .image-lightbox-control / -counter. */
.image-lightbox.lb-flip-out .image-lightbox-control,
.image-lightbox.lb-flip-out .image-lightbox-counter {
    opacity: 0;
    pointer-events: none;
}

.image-lightbox-close {
    position: absolute;
    /* The lightbox is position:fixed against the viewport, so on notched iOS the
       top sits under the status bar / Dynamic Island. Inset below it. env() is 0
       on desktop/web (no-op). Mirrors the messenger-header pattern. */
    top: calc(max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) + 14px);
    right: 20px;
    color: white;
    font-size: 2rem;
    cursor: pointer;
    /* Dark scrim, not translucent white: the white glyph must stay legible over a
       BRIGHT photo, where a white-on-white wash made the button near-invisible. */
    background: rgba(0,0,0,0.55);
    border: none;
    border-radius: 6px;
    /* Asymmetric vertical padding (3 top / 5 bottom, same 8px total height as
       4/4) nudges the × glyph 1px up within the square without resizing the box —
       the glyph's ink sits slightly low in its line box. */
    padding: 3px 10px 5px;
    line-height: 1;
    z-index: 2001;
}
/* contentInset:"always" builds: the OS already insets the webview below the
   notch, so env() would double it — keep the plain 14px there. */
html.cap-inset-always .image-lightbox-close { top: 14px; }
/* CSS-floor fallback: restore the floor value dropped by the zeroing rule
   above (see the css/style-03.css three-rule pattern comment). Ported from
   canopy 2026-08-11. */
html.cap-inset-always.cap-inset-floor .image-lightbox-close { top: calc(var(--sat-floor, 0px) + 14px); }

/* Save button — bottom-right, clear of the iOS home indicator (safe-area). */
.image-lightbox-save {
    position: absolute;
    bottom: calc(max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)) + 16px);
    right: 20px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 38px;
    color: #fff;
    cursor: pointer;
    /* Dark scrim (matches .image-lightbox-close) so the Save glyph stays visible
       over a bright photo, where the old translucent white washed it out. */
    background: rgba(0,0,0,0.55);
    border: none;
    border-radius: 6px;
    z-index: 2001;
    transition: background 0.15s, opacity 0.15s;
}
html.cap-inset-always .image-lightbox-save { bottom: 16px; }
/* CSS-floor fallback: restore the floor value dropped by the zeroing rule
   above (see the css/style-03.css three-rule pattern comment). Ported from
   canopy 2026-08-11. */
html.cap-inset-always.cap-inset-floor .image-lightbox-save { bottom: calc(var(--sab-floor, 0px) + 16px); }
.image-lightbox-save:hover { background: rgba(0,0,0,0.72); }
.image-lightbox-save.lb-saving { opacity: 0.45; pointer-events: none; }
/* Brief green pulse on the Save button the moment a save lands. */
.image-lightbox-save.lb-saved-flash { background: rgba(74,222,128,0.9); }

/* "Saved" confirmation toast — floats above the Save button, fades in/out. */
.image-lightbox-toast {
    position: absolute;
    bottom: calc(max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)) + 78px);
    left: 50%;
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: 80vw;
    background: rgba(18,18,18,0.86);
    color: #fff;
    font-size: var(--font-size-small);
    line-height: 1;
    padding: 10px 16px;
    border-radius: 999px;
    z-index: 2002;
    opacity: 0;
    pointer-events: none;
    transform: translateX(-50%) translateY(8px);
    transition: opacity 0.2s ease, transform 0.2s ease;
}
html.cap-inset-always .image-lightbox-toast { bottom: 78px; }
/* CSS-floor fallback: restore the floor value dropped by the zeroing rule
   above (see the css/style-03.css three-rule pattern comment). Mirrors the
   base rule's additive calc(max(env, floor) + 78px) with env() removed.
   Ported from canopy 2026-08-11. */
html.cap-inset-always.cap-inset-floor .image-lightbox-toast { bottom: calc(var(--sab-floor, 0px) + 78px); }
.image-lightbox-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.image-lightbox-toast svg { width: 17px; height: 17px; color: #4ade80; flex-shrink: 0; }

/* Lightbox carousel — arrows hidden (swipe-only navigation, Telegram-style).
   The button elements stay in the DOM so the keyboard ArrowLeft/Right handler and
   the swipe-end fallback path that look them up by id keep working. */
.image-lightbox-prev,
.image-lightbox-next { display: none; }

/* Lightbox carousel - counter */
.image-lightbox-counter {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    color: rgba(255,255,255,0.85);
    font-size: var(--font-size-body);
    pointer-events: none;
}

/* Flick-to-dismiss: a vertical drag on the photo (handled in image-crop.js)
   closes the lightbox the way Telegram's media viewer does: the image tracks
   the finger 1:1 while JS sets its transform + the backdrop alpha inline. Here
   we only fade the chrome out of the way so the photo and the dimming backdrop
   are all that remain during the gesture. The transition also fades the chrome
   back in on a spring-back. */
.image-lightbox-close,
.image-lightbox-save,
.image-lightbox-prev,
.image-lightbox-next,
.image-lightbox-counter {
    transition: opacity 0.15s ease, background 0.15s ease;
}
.image-lightbox.lb-dragging .image-lightbox-control,
.image-lightbox.lb-dragging .image-lightbox-counter {
    opacity: 0;
    pointer-events: none;
}

/* ── Video lightbox ──────────────────────────────────────────────────────
   Telegram-style player. Chrome (backdrop, close X top right, save bottom
   right) is the SAME markup pattern and the SAME .image-lightbox-close /
   .image-lightbox-save classes as the photo viewer above, so the two feel
   identical; only the stage in the middle differs. */
.video-lightbox {
    flex-direction: column;
}
.video-lightbox.show {
    animation: lightboxBackdropIn 200ms var(--ease-out);
}
.video-lightbox.show .video-lightbox-stage video {
    animation: lightboxImgIn 260ms var(--ease-spring);
}

.video-lightbox-stage {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    max-height: 82vh;
    cursor: pointer;
}
.video-lightbox-stage video {
    max-width: 90vw;
    max-height: 78vh;
    object-fit: contain;
    border-radius: 8px;
    background: #000;
}

/* Centre transport: skip-back-15 / play-pause / skip-forward-15, large and
   overlaid on the video (Telegram's media viewer). */
.video-lightbox-center-controls {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    gap: 34px;
    z-index: 2001;
    transition: opacity 0.18s ease;
}
.video-lightbox-ctrl-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: rgba(0, 0, 0, 0.4);
    color: #fff;
    border-radius: 50%;
    cursor: pointer;
}
.video-lightbox-playpause {
    width: 72px;
    height: 72px;
    font-size: 30px;
}
.video-lightbox-playpause i { margin-left: 2px; } /* optical centring of the play glyph */
.video-lightbox-playpause.playing i { margin-left: 0; }
.video-lightbox-skip {
    width: 52px;
    height: 52px;
    font-size: 22px;
}
/* The "15" sits inside the circular-arrow glyph, mirroring Telegram's skip icon. */
.video-lightbox-skip span {
    position: absolute;
    left: 50%;
    top: 54%;
    transform: translate(-50%, -50%);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
    pointer-events: none;
}

/* Time row: elapsed left, scrub bar middle, duration right. */
.video-lightbox-time-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: min(92vw, 640px);
    margin: 14px auto max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px), 14px);
    padding: 0 4px;
    z-index: 2001;
    transition: opacity 0.18s ease;
}
html.cap-inset-always .video-lightbox-time-row { margin-bottom: 14px; }
/* CSS-floor fallback: restore the floor value dropped by the zeroing rule
   above (see the css/style-03.css three-rule pattern comment). Mirrors the
   base rule's three-arg max(env, floor, 14px) with env() removed. Ported
   from canopy 2026-08-11. */
html.cap-inset-always.cap-inset-floor .video-lightbox-time-row { margin-bottom: max(var(--sab-floor, 0px), 14px); }
.video-lightbox-time {
    color: rgba(255, 255, 255, 0.9);
    font-size: var(--font-size-small);
    font-variant-numeric: tabular-nums;
    min-width: 38px;
    flex-shrink: 0;
}
.video-lightbox-time#videoLightboxDuration { text-align: right; }
.video-lightbox-progress {
    position: relative;
    flex: 1;
    height: 20px; /* generous hit target; the visible track is thin */
    display: flex;
    align-items: center;
    cursor: pointer;
    touch-action: none;
}
.video-lightbox-progress-track {
    position: absolute;
    left: 0;
    right: 0;
    height: 4px;
    border-radius: 2px;
    background: rgba(255, 255, 255, 0.25);
}
.video-lightbox-progress-fill {
    position: absolute;
    left: 0;
    height: 4px;
    border-radius: 2px;
    background: #fff;
    width: 0%;
    pointer-events: none;
}
.video-lightbox-progress-knob {
    position: absolute;
    left: 0%;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
    transform: translateX(-50%);
    pointer-events: none;
}
.video-lightbox-progress.dragging .video-lightbox-progress-knob { transform: translateX(-50%) scale(1.25); }

/* Tap-to-hide: JS toggles this on the lightbox root. The video itself and the
   backdrop stay put, only the chrome fades, same idea as the photo lightbox's
   .lb-dragging control fade. */
.video-lightbox.controls-hidden .image-lightbox-close,
.video-lightbox.controls-hidden .image-lightbox-save,
.video-lightbox.controls-hidden .video-lightbox-center-controls,
.video-lightbox.controls-hidden .video-lightbox-time-row,
.video-lightbox.controls-hidden .video-lightbox-caption {
    opacity: 0;
    pointer-events: none;
}

/* Lightbox caption (image + video): shown only when the tapped-through row's
   content is non-empty (grid tiles themselves never show captions, Media subtab,
   2026-07-26, port of chatbox 775c353). */
.image-lightbox-caption,
.video-lightbox-caption {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    padding: 14px 20px calc(20px + max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)));
    background: linear-gradient(to top, rgba(0,0,0,0.65), rgba(0,0,0,0));
    color: #fff;
    font-size: var(--font-size-body);
    line-height: 1.4;
    text-align: center;
    pointer-events: none;
    z-index: 5;
}
.video-lightbox-caption { padding-bottom: calc(64px + max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px))); } /* clears the time row */

/* Cropper modal z-index (above other modals) */
#imageCropperModal { z-index: 99999 !important; }
@media (max-width: 480px) { #cropperContainer { height: 220px !important; } }

/* Utilities */
.text-center { text-align: center; }
.text-muted { color: var(--color-text-light); }
.empty-state-text { color: var(--color-text-light); font-style: italic; font-size: var(--font-size-body); }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.mb-8 { margin-bottom: 8px; }
.mb-16 { margin-bottom: 16px; }
.hidden { display: none !important; }

/* =====================================================
   More Page Nav block (formerly the slide-out drawer)
   The hamburger button, backdrop overlay, and drawer chrome were removed when
   the bottom nav bar + More tab replaced the drawer. buildNavMenuSections now
   fills #navMenu in-page inside tab-more; the tab's own background shows
   through, so this block is transparent with no position/transform/transition.
   The bottom-nav bar styling lives in Section 5 (Bottom Nav Bar).
   ===================================================== */

.nav-menu {
    display: block;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
}

.nav-menu-profile {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 20px 16px 12px;
    border-bottom: 1px solid var(--color-border);
}

.nav-menu-profile-img {
    width: 128px;
    height: 128px;
    border-radius: 50%;
    object-fit: cover;
}

.nav-menu-profile-placeholder {
    width: 128px;
    height: 128px;
    border-radius: 50%;
    background: var(--color-border);
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-primary);
}

.nav-menu-profile-name {
    margin-top: 8px;
    font-size: var(--font-size-body);
    font-weight: 600;
    color: var(--color-text);
}

.nav-menu-section {
    padding: 8px 16px;
    border-bottom: 1px solid var(--color-border);
}

.nav-menu-section:last-child {
    border-bottom: none;
}

/* Plain text links only (Edit Profile, Sign Out). Anchor tabs like Messenger
   carry .nav-menu-tab and must keep its flex layout - without the :not() this
   rule's higher specificity (0,1,1) turns them block, killing the icon gap. */
.nav-menu-section a:not(.nav-menu-tab) {
    display: block;
    padding: 10px 12px;
    color: var(--color-text);
    text-decoration: none;
    font-family: inherit;
    font-size: var(--font-size-body);
    font-weight: 500;
    border-radius: 8px;
    transition: background 0.15s var(--ease-out);
}

@media (hover: hover) {
    .nav-menu-section a:hover {
        background: rgba(232, 105, 30, 0.06);
    }
}

.nav-menu-tab {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 10px 12px;
    background: none;
    border: none;
    border-radius: 8px;
    text-align: left;
    font-family: inherit;
    font-size: var(--font-size-body);
    font-weight: 500;
    color: var(--color-text);
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s var(--ease-out);
    box-sizing: border-box;
}

@media (hover: hover) {
    .nav-menu-tab:hover {
        background: rgba(232, 105, 30, 0.06);
    }
}

.nav-menu-tab.active {
    color: var(--color-primary);
    background: rgba(232, 105, 30, 0.08);
    font-weight: 600;
}

.nav-menu-section .view-toggle {
    justify-content: center;
}

/* Hide empty mobile menu sections */
.nav-menu-section:empty {
    display: none;
    padding: 0;
    border-bottom: none;
}

.nav-menu-section-label {
    font-size: var(--font-size-subheading);
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--color-text-light);
    padding: 4px 12px 2px;
    display: block;
}

/* Doubled class (0,2,1) keeps the destructive red above
   .nav-menu-section a:not(.nav-menu-tab), same specificity, later in source. */
.nav-menu-section.nav-menu-signout a {
    display: block;
    padding: 10px 12px;
    color: var(--color-error);
    text-decoration: none;
    font-size: var(--font-size-body);
    font-weight: 500;
    border-radius: 8px;
    transition: background 0.15s var(--ease-out);
}

@media (hover: hover) {
    .nav-menu-signout a:hover {
        background: rgba(192, 57, 43, 0.06);
    }
}

/* Responsive - desktop enhancements */
@media (min-width: 1025px) {
    .app-container {
        padding: 24px 40px;
        max-width: 1000px;
    }

    .card {
        padding: 32px;
    }
}

/* Phone-only compact styling */
@media (max-width: 767px) {
    body {
        font-size: var(--font-size-body);
    }

    .app-container {
        padding: 0;
    }

    .app-header {
        padding: 12px 12px 8px;
    }

    /* contentInset:"always" builds (Myron's device report, 2026-08-02): the OS is
       assumed to shift normal-flow content below the notch, so body's own env()
       padding-top is suppressed for cap-inset-always (see the "Safe area for
       notched phones" block in style-03.css) to avoid double-insetting it. That
       shift only reliably happens once the page has scrollable content; .app-header
       is the very first thing painted on every cold boot/tab load, often before the
       shift has anything to apply to, so it rendered under the status bar / Dynamic
       Island. Give the header its OWN env() inset, additive over its existing
       padding-top, scoped to cap-inset-always only: body's padding-top there is
       already suppressed to 0, so this cannot double-count, and it changes nothing
       on web/edge-to-edge (body already supplies the single correct inset there via
       the unconditional @supports rule) or on Android cap-inset-always (no real
       notch, env() is 0). Same class of fix as the messenger .msgr-thread-header
       correction above and chatbox's f6255e7/13fb940. */
    html.cap-inset-always .app-header {
        padding-top: calc(max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) + 12px);
    }

    .app-header .logo {
        font-size: var(--font-size-heading);
    }

    .app-header .logo small {
        font-size: var(--font-size-subheading);
    }

    .nav-bar {
        padding: 0 12px 6px;
        margin-bottom: 8px;
    }

    .nav-bar .user-info {
        font-size: var(--font-size-small);
    }

    .view-toggle {
        font-size: var(--font-size-subheading);
    }

    .view-toggle-btn {
        padding: 4px 10px;
    }

    .card {
        border-radius: 0;
        border-left: none;
        border-right: none;
        border-top: none;
        box-shadow: none;
        margin-bottom: 0;
        border-bottom: 1px solid var(--color-border);
        padding: 16px 12px;
    }

    .data-table {
        font-size: var(--font-size-small);
    }
    .data-table th,
    .data-table td {
        padding: 8px 6px;
    }

    /* Users tab: collapse Role and Parent columns into name cell on mobile */
    .user-col-role, .user-col-parent { display: none; }
    .user-mobile-badges { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 4px; }
    #tab-users .data-table td:last-child,
    #tab-users .data-table th:last-child { padding-right: 8px; }

    /* History tab: allow Details column to wrap rather than squish */
    #tab-history .data-table td:last-child {
        white-space: normal;
        word-break: break-word;
        min-width: 80px;
    }


    /* Detail rows: stack label + value on mobile when editing */
    .detail-row {
        flex-wrap: wrap;
    }

    .detail-edit-form {
        width: 100%;
    }

    .modal {
        padding: 20px 16px;
    }

    .modal-cf-sign {
        padding: 0;
    }

    .modal-cf-sign .modal {
        border-radius: 0;
        max-height: 100svh;
    }

    /* Full-screen-on-mobile modals (broadcast, new announcement): fill the whole
       viewport so tall forms use all the space, and keep the title/close clear of
       the status bar / notch. Mirrors the messenger fixed-header dual pattern: on
       web use env(safe-area-inset-top); on shipped native builds (.cap-inset-always)
       the OS already insets the webview below the notch, so env() would double it,
       use a plain inset there. --kb-inset shrinks the modal above the soft keyboard
       (0 at rest / on desktop). */
    .modal-overlay.modal-fullscreen-mobile {
        /* Keep the soft-keyboard reservation: a bare `padding: 0` here wiped the
           base overlay's padding-bottom kb-inset lift, so the shrunken modal
           flex-centred in the FULL viewport and its bottom (incl. the submit
           button) sat under the keyboard. 0 at rest, so no visual change. */
        padding: 0 0 var(--kb-inset, 0px);
    }
    .modal-fullscreen-mobile .modal {
        width: 100%;
        max-width: 100%;
        height: 100svh;
        max-height: calc(100svh - var(--kb-inset, 0px));
        border-radius: 0;
        box-shadow: none;
        padding-top: calc(max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) + 20px);
        padding-bottom: calc(max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)) + 20px);
    }
    html.cap-inset-always .modal-fullscreen-mobile .modal {
        padding-top: 20px;
        padding-bottom: 20px;
    }
    /* CSS-floor fallback: restore the floor values dropped by the zeroing rule
       above (see the css/style-03.css three-rule pattern comment). Ported
       from canopy 2026-08-11. */
    html.cap-inset-always.cap-inset-floor .modal-fullscreen-mobile .modal {
        padding-top: calc(var(--sat-floor, 0px) + 20px);
        padding-bottom: calc(var(--sab-floor, 0px) + 20px);
    }

    /* Payslip detail modal: fills the screen on mobile (was a floating card
       with margin all round). The close X sits over the dark payslip header
       band once full-screen, so it gets its own contrast-safe circular
       backdrop + 44px touch target rather than the plain floated global
       .modal-close, which reads invisible (dark-on-dark) there. Header text
       sizes come down to the font-size token scale (was fixed px, oversized
       on a phone width). Ported deliberately from canopy 2026-08-09 (commit
       5c8b795a and the four follow-up commits refining it). */
    #payslipDetailModal.modal-overlay {
        padding: 0;
    }
    #payslipDetailModal .modal {
        position: relative;
        width: 100%;
        max-width: 100%;
        height: 100svh;
        max-height: 100svh;
        border-radius: 0;
        box-shadow: none;
        padding: 0;
        overflow-y: auto;
    }
    #payslipDetailModal .modal-close {
        position: absolute;
        top: calc(max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) + 12px);
        right: 12px;
        width: 44px;
        height: 44px;
        min-width: 44px;
        border-radius: 50%;
        background: rgba(0, 0, 0, 0.35);
        color: #fff;
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: var(--font-size-heading);
        line-height: 1;
        float: none;
        z-index: 10;
    }
    html.cap-inset-always #payslipDetailModal .modal-close {
        top: 12px;
    }
    /* CSS-floor fallback: restore the floor value dropped by the zeroing rule
       above (see the css/style-03.css three-rule pattern comment). Ported
       from canopy 2026-08-11. */
    html.cap-inset-always.cap-inset-floor #payslipDetailModal .modal-close {
        top: calc(var(--sat-floor, 0px) + 12px);
    }
    #payslipDetailModal .payslip-pdf {
        border-radius: 0;
        border: none;
        min-height: 100%;
    }
    #payslipDetailModal .payslip-pdf-company {
        font-size: var(--font-size-heading);
        padding-right: 44px;   /* clear the close button */
    }
    #payslipDetailModal .payslip-pdf-uen {
        font-size: var(--font-size-small);
    }
    #payslipDetailModal .payslip-pdf-title {
        font-size: var(--font-size-heading);
    }

    /* Safe-area clearance for the modal's CONTENT (the close button above
       already has its own). No named --safe-area-* custom property exists in
       this codebase (env() is inlined at every call site). Top: pad the navy
       header band itself (not the modal), so the navy colour still reaches
       the very edge behind the status bar/notch while the company name and
       UEN sit below it (pad, don't offset, keeps the edge-to-edge look).
       Bottom: horizontal padding plus full-width stacked buttons per the
       app's mobile action-row convention, and clear the home-indicator curve
       once scrolled to the end. */
    #payslipDetailModal .payslip-pdf-header {
        padding-top: calc(max(env(safe-area-inset-top, 0px), var(--sat-floor, 0px)) + 24px);
    }
    html.cap-inset-always #payslipDetailModal .payslip-pdf-header {
        padding-top: 24px;
    }
    /* CSS-floor fallback: restore the floor value dropped by the zeroing rule
       above (see the css/style-03.css three-rule pattern comment). Ported
       from canopy 2026-08-11. */
    html.cap-inset-always.cap-inset-floor #payslipDetailModal .payslip-pdf-header {
        padding-top: calc(var(--sat-floor, 0px) + 24px);
    }
    #payslipDetailModal .payslip-detail-actions {
        padding: 14px 20px calc(max(env(safe-area-inset-bottom, 0px), var(--sab-floor, 0px)) + 20px);
    }
    html.cap-inset-always #payslipDetailModal .payslip-detail-actions {
        padding: 14px 20px 20px;
    }
    /* CSS-floor fallback: restore the floor value dropped by the zeroing rule
       above (see the css/style-03.css three-rule pattern comment). Ported
       from canopy 2026-08-11. */
    html.cap-inset-always.cap-inset-floor #payslipDetailModal .payslip-detail-actions {
        padding: 14px 20px calc(var(--sab-floor, 0px) + 20px);
    }
    #payslipDetailModal .payslip-detail-actions .btn {
        flex: 1;
        min-height: 44px;
    }

    .modal-hero {
        padding: 0;
    }

    .modal-hero-content {
        padding: 20px 16px 24px;
    }

    .modal-hero .enrich-carousel {
        border-radius: 12px 12px 0 0;
    }

    .settings-school-item {
        gap: 8px;
    }

    .settings-school-item .school-item-name {
        font-size: var(--font-size-small);
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .school-item .school-name {
        font-size: var(--font-size-small);
    }

    .school-item .btn,
    .school-item-actions .btn {
        font-size: var(--font-size-small);
        padding: 4px 8px;
    }

    #newSchoolName {
        font-size: 0.75rem !important;
        padding: 5px 8px !important;
    }

    /* Settings tab - mobile compact */
    #tab-settings .card h2 {
        font-size: var(--font-size-body);
        margin-bottom: 8px;
    }

    #tab-settings .card > p.text-muted {
        font-size: var(--font-size-subheading);
    }

    #tab-settings .form-group {
        margin-bottom: 10px;
    }

    #tab-settings .form-group label {
        font-size: var(--font-size-subheading);
        margin-bottom: 4px;
    }

    #tab-settings .form-group select,
    #tab-settings .form-group input {
        font-size: var(--font-size-small);
        padding: 7px 10px;
    }

    #tab-settings .btn {
        font-size: var(--font-size-subheading);
        padding: 7px 14px;
    }

    #tab-settings .btn.btn-small {
        font-size: var(--font-size-small);
        padding: 4px 8px;
    }

    /* History tab - mobile compact */
    #tab-history .btn {
        font-size: var(--font-size-small);
        padding: 7px 14px;
    }
}

/* Phone input group (country code + phone field) */
.phone-input-group {
    display: flex;
    gap: 8px;
    align-items: stretch;
}

.phone-input-group .country-code-select {
    width: 88px !important;
    min-width: 88px;
    max-width: 88px;
    flex: 0 0 88px;
    padding: 12px 28px 12px 14px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    font-size: var(--font-size-body);
    background: var(--color-card);
    color: var(--color-text);
    text-align: left;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B6B6B' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
}

.phone-input-group .country-code-select:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(232, 105, 30, 0.15);
}

.phone-input-group input[type="tel"] {
    flex: 1 1 auto;
    width: auto !important;
    min-width: 0;
    padding: 12px 14px;
    border: 1px solid var(--color-border);
    border-radius: 8px;
    font-size: var(--font-size-body);
    background: var(--color-card);
    color: var(--color-text);
}

.phone-input-group input[type="tel"]:focus {
    outline: none;
    border-color: var(--color-primary);
    box-shadow: 0 0 0 3px rgba(232, 105, 30, 0.15);
}

/* Form row (side-by-side fields) */
.form-row {
    display: flex;
    gap: 12px;
}

.form-row .form-group {
    flex: 1;
    min-width: 0;
}

/* Badge info variant */
.badge-info {
    background: rgba(232, 105, 30, 0.12);
    color: var(--color-primary-dark);
}

/* Welcome card preview */
.welcome-card-preview {
    text-align: center;
    padding: 16px;
    background: var(--color-bg);
    border-radius: 8px;
    min-height: 100px;
}

.welcome-card-preview img {
    max-width: 100%;
    border-radius: 8px;
    box-shadow: var(--color-shadow);
}

/* Welcome card - Messenger conversation picker */
.card-conv-picker-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 16px;
}
.card-conv-picker-header h2 { margin: 0; }
.card-conv-list {
    max-height: 320px;
    overflow-y: auto;
}
.card-conv-list--sending {
    opacity: 0.5;
    pointer-events: none;
}
.card-conv-row {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 12px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s;
}
.card-conv-row:hover { background: var(--color-surface-alt); }
.card-conv-row-name {
    display: flex;
    align-items: center;
    gap: 6px;
    font-weight: 500;
    font-size: var(--font-size-body);
}
.card-conv-row-preview {
    font-size: 12px;
    color: var(--color-text-light);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.card-conv-wa-badge {
    font-size: 10px;
    background: #25D366;
    color: #fff;
    border-radius: 4px;
    padding: 1px 5px;
    line-height: 1.4;
    flex-shrink: 0;
}

/* Parent detail modal */
.parent-detail-info {
    background: var(--color-bg);
    border-radius: 8px;
    padding: 16px;
    margin-bottom: 16px;
}

.detail-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 0;
    gap: 16px;
    border-bottom: 1px solid var(--color-border);
}

.detail-row:last-child {
    border-bottom: none;
}

.detail-label {
    font-weight: 600;
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    flex-shrink: 0;
}

.detail-value {
    font-size: var(--font-size-body);
    color: var(--color-text);
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
    flex: 1;
}

.detail-value > span:first-child {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.detail-value a {
    color: var(--color-primary);
}

.detail-edit-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--color-text-light);
    font-size: var(--font-size-label);
    padding: 8px 10px;
    border-radius: 4px;
    transition: color 0.2s, background 0.2s;
}

.detail-edit-btn:hover {
    color: var(--color-primary);
    background: rgba(232, 105, 30, 0.08);
}

.detail-row:has(.detail-edit-btn):hover .detail-edit-btn {
    color: var(--color-primary);
}

.detail-row.editing .detail-edit-btn {
    display: none;
}

.detail-row.editing {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 8px;
    align-items: center;
}

.detail-row.editing .detail-label {
    grid-row: 1;
    grid-column: 1;
}

.detail-row.editing .detail-edit-form {
    grid-row: 1;
    grid-column: 2;
    justify-self: end;
}

.detail-row.editing .detail-value {
    grid-row: 2;
    grid-column: 1 / -1;
    overflow: visible;
}

.detail-edit-form {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-shrink: 0;
}


.detail-edit-input {
    flex: 1;
    min-width: 0;
    max-width: 100%;
    padding: 6px 10px;
    border: 1px solid var(--color-primary);
    border-radius: 6px;
    font-size: var(--font-size-body);
    outline: none;
    box-shadow: 0 0 0 2px rgba(232, 105, 30, 0.15);
    box-sizing: border-box;
}

select.detail-edit-input {
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    padding-right: 28px;
    background-color: var(--color-card);
    color: var(--color-text);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B6B6B' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 8px center;
    cursor: pointer;
    text-overflow: ellipsis;
}

/* Compact phone input inside detail edit modals */
.detail-value .country-code-select {
    width: 64px !important;
    min-width: 64px;
    max-width: 64px;
    flex: 0 0 64px;
    padding: 6px 22px 6px 8px;
    font-size: var(--font-size-body);
    border: 1px solid var(--color-primary);
    border-radius: 6px;
    box-shadow: 0 0 0 2px rgba(232, 105, 30, 0.15);
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-color: var(--color-card);
    color: var(--color-text);
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236B6B6B' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 6px center;
    cursor: pointer;
}

.detail-edit-save,
.detail-edit-cancel {
    background: none;
    border: none;
    cursor: pointer;
    font-size: var(--font-size-body);
    padding: 8px 10px;
    border-radius: 4px;
    line-height: 1;
}

.detail-edit-save {
    color: var(--color-success);
}

.detail-edit-save:hover {
    background: rgba(46, 139, 87, 0.1);
}

.detail-edit-cancel {
    color: var(--color-error);
}

.detail-edit-cancel:hover {
    background: rgba(192, 57, 43, 0.1);
}

/* Clickable table rows */
/* Users tab - filter bar always visible; badges only on mobile */
.users-filter-bar { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
@media (min-width: 768px) {
    .user-mobile-badges { display: none; }
}

.data-table tr.clickable-row {
    cursor: pointer;
}

.data-table tr.clickable-row:hover td {
    background-color: rgba(232, 105, 30, 0.06);
}

/* Child list in detail modal */
.detail-child-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 0;
    border-bottom: 1px solid var(--color-border);
}

.detail-child-item:last-child {
    border-bottom: none;
}

/* Modal success state */
.modal-success {
    text-align: center;
    padding: 32px 16px;
}

.modal-success .success-icon {
    width: 64px;
    height: 64px;
    border-radius: 50%;
    background: #E8F5E9;
    color: var(--color-success);
    font-size: 2rem;
    display: flex;
    align-items: center;
    justify-content: center;
    margin: 0 auto 16px;
    animation: successPop 0.4s ease-out;
}

.modal-success .success-message {
    font-size: var(--font-size-heading);
    font-weight: 600;
    color: var(--color-success);
    margin-bottom: 8px;
}

.modal-success .success-sub {
    font-size: var(--font-size-body);
    color: var(--color-text-light);
    margin-bottom: 24px;
}

/* Ticket Cards */
.ticket-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 0;
    border-bottom: 1px solid var(--color-border);
}

.ticket-card:last-child {
    border-bottom: none;
}

.ticket-card-info {
    flex: 1;
    min-width: 0;
}

.ticket-card-date {
    font-weight: 600;
    font-size: var(--font-size-body);
    color: var(--color-text);
}

.ticket-card-count {
    font-size: var(--font-size-label);
    color: var(--color-text-light);
    margin-top: 2px;
}

