/* SinghaSport — component styles (plain CSS; loaded after the pre-built tailwind.css).
   Palette: Enless Golf Center C.I. 2026 — Navy #152A50 (structure), Academy Blue
   #265EA9 (interaction), Cloud Grey #DBDDE3 (surfaces). Semantic green/amber/red
   are kept as-is so status stays readable. Keep in sync with tailwind.config.js. */

:root {
    --brand:       #265ea9;
    --brand-dark:  #1e4d8a;
    --brand-light: #eef3fa;
    --ring:        rgba(38, 94, 169, .30);
    --accent:      #265ea9;
    --accent-dark: #1e4d8a;
    /* Text that sits ON an accent fill (primary button, count badge, selected chip). */
    --ink:         #ffffff;

    /* Ink ramp for body copy. Replaces the slate neutrals everywhere except the
       sidebar and the topbar — see the "Navy text tone" section at the bottom of
       this file. Darkest → lightest. */
    --ink-900:     #0f1e3a;
    --ink-800:     #1a2438;
    --ink-700:     #2b3750;
    --ink-600:     #4a5568;
    --ink-500:     #6b7488;
    --ink-400:     #8a93a6;
    --ink-300:     #a6adbd;
}

html { scroll-behavior: smooth; }

/* Cloud page background — a soft diagonal wash, light enough that white cards still
   read as raised on top of it. Fixed so it covers the viewport regardless of scroll. */
.bg-page {
    background: linear-gradient(155deg, #f7f8fa 0%, #f2f3f6 45%, #e9ebf1 100%) fixed;
}
body { -webkit-tap-highlight-color: transparent; }

/* Scrollbars */
* { scrollbar-width: thin; scrollbar-color: #dbdde3 transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-thumb { background: #dbdde3; border-radius: 9px; }

/* ---------------------------------------------------------------- Buttons */
.btn {
    display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
    font-weight: 500; font-size: .875rem; line-height: 1.25rem;
    padding: .55rem .9rem; border-radius: .65rem; border: 1px solid transparent;
    cursor: pointer; transition: background .15s, color .15s, border-color .15s, box-shadow .15s;
    white-space: nowrap; text-decoration: none;
}
.btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.btn[disabled], .btn.is-loading { opacity: .55; cursor: not-allowed; }
.btn-sm { padding: .38rem .65rem; font-size: .8rem; border-radius: .55rem; }
.btn-primary { background: var(--accent); color: var(--ink); font-weight: 600; }
.btn-primary:hover { background: var(--accent-dark); }

/* Accent chrome — unifies the interactive/attention accent (count badges,
   avatar/initial circles, selected chips) with the Academy Blue primary buttons.
   The count badge also has to read against the navy sidebar, which is why it is a
   filled blue pill with white text rather than a tint. Semantic status pills
   (success/warning/danger) are intentionally NOT part of this. */
.badge-count { background: var(--accent); color: var(--ink); }
.avatar-accent { background: #dde7f4; color: #152a50; }
.chip-selected { background: var(--accent); color: var(--ink); border-color: var(--accent-dark) !important; }
.bg-gold { background: var(--accent) !important; }
.btn-secondary { background: #fff; color: #3d465c; border-color: #e6e8ee; }
.btn-secondary:hover { background: #f7f8fa; }
.btn-danger { background: #dc2626; color: #fff; }
.btn-danger:hover { background: #b91c1c; }
.btn-ghost { background: transparent; color: #4a5568; }
.btn-ghost:hover { background: #f2f3f6; }

/* ----------------------------------------------------------------- Inputs */
.input, .select, .textarea {
    width: 100%; padding: .55rem .75rem; font-size: .875rem;
    border-radius: .65rem; border: 1px solid #e6e8ee; background: #fff; color: #0f1e3a;
    transition: border-color .15s, box-shadow .15s;
}
.input:focus, .select:focus, .textarea:focus {
    outline: none; border-color: var(--brand); box-shadow: 0 0 0 3px var(--ring);
}
.input::placeholder { color: #8a93a6; }
.input[readonly], .input:disabled { background: #f2f3f6; color: #4a5568; }
.label { display: block; font-size: .8rem; font-weight: 500; color: #4a5568; margin-bottom: .35rem; }
.form-hint { font-size: .75rem; color: #8a93a6; margin-top: .3rem; }
.form-error { font-size: .75rem; color: #dc2626; margin-top: .3rem; }

/* Filter-bar fields: readable width on desktop, full-width stack on mobile.
   Used inside `flex flex-wrap items-end gap-3` toolbars (reports + list filters). */
.field { flex: 1 1 12rem; min-width: 12rem; }
.field > .input, .field > .select, .field > .textarea { width: 100%; }
.field-actions { flex: 1 1 100%; display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: flex-end; gap: .5rem; }
@media (max-width: 640px) {
    .field { flex-basis: 100%; min-width: 0; }
    .field-actions { justify-content: stretch; }
    .field-actions > .btn { flex: 1 1 auto; justify-content: center; }
}

/* ------------------------------------------------------------------ Cards */
.card { background: #fff; border: 1px solid #e6e8ee; border-radius: 1rem; box-shadow: 0 1px 2px rgba(21, 42, 80, .06); }
.card-p { padding: 1.25rem; }

/* ------------------------------------------------------------------ Table */
.table-wrap { overflow-x: auto; border-radius: 1rem; }
.table { width: 100%; border-collapse: collapse; font-size: .875rem; }
.table thead th {
    text-align: left; font-weight: 600; color: #4a5568; background: #f7f8fa;
    padding: .7rem .9rem; border-bottom: 1px solid #e6e8ee; white-space: nowrap;
}
.table tbody td { padding: .7rem .9rem; border-bottom: 1px solid #f2f3f6; color: #3d465c; vertical-align: middle; }
.table tbody tr:last-child td { border-bottom: none; }
.table tbody tr:hover { background: #f7f8fa; }
.table tfoot td { padding: .7rem .9rem; font-weight: 700; background: #f7f8fa; border-top: 2px solid #e6e8ee; color: #0f1e3a; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.center { text-align: center !important; }

/* SweetAlert2 — recolor the default amber "warning" icon to neutral slate (no orange in the palette). */
.swal2-icon.swal2-warning { border-color: #dbdde3; color: #4a5568; }

/* -------------------------------------------------------------- Select2 */
.select2-container { width: 100% !important; }
.select2-container--default .select2-selection--single {
    height: 2.5rem; border: 1px solid #dbdde3; border-radius: .5rem; background: #fff;
    display: flex; align-items: center;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
    line-height: 2.5rem; padding-left: .75rem; padding-right: 1.8rem; color: #0f1e3a;
}
.select2-container--default .select2-selection--single .select2-selection__arrow { height: 2.4rem; right: .4rem; }
.select2-container--default.select2-container--focus .select2-selection--single,
.select2-container--default.select2-container--open .select2-selection--single {
    border-color: #265ea9; box-shadow: 0 0 0 2px rgba(38, 94, 169, .25);
}
/* Multi-select (course_form's "โปรผู้สอนที่สอนคอร์สนี้ได้") — the same box as the
   single dropdowns it sits between, growing a row at a time as tags are added.
   Select2 lays the tags out with floats and sizes its inline search field from
   JS, so only the skin is restyled here — changing the layout to flex loses the
   placeholder, whose width that JS measures. */
.select2-container--default .select2-selection--multiple {
    min-height: 2.5rem; border: 1px solid #dbdde3; border-radius: .5rem;
    background: #fff; padding: 0 .15rem .25rem;
}
.select2-container--default.select2-container--focus .select2-selection--multiple,
.select2-container--default.select2-container--open .select2-selection--multiple {
    border: 1px solid #265ea9; box-shadow: 0 0 0 2px rgba(38, 94, 169, .25);
}
/* Block, not select2's default inline-block: an inline-block list adds a
   baseline gap under the tags, which made the empty box taller than the single
   dropdowns beside it. */
.select2-container--default .select2-selection--multiple .select2-selection__rendered { display: block; }
.select2-container--default .select2-selection--multiple .select2-selection__choice {
    margin-top: .3rem; margin-right: .3rem; padding: .1rem .45rem;
    font-size: .8125rem; line-height: 1.35rem;
    background: #dde7f4; border: 1px solid #c3d5ea; border-radius: .375rem; color: #1e4d8a;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
    margin-right: .3rem; color: #1e4d8a;
}
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { color: #0f1e3a; }
.select2-container--default .select2-selection--multiple .select2-search--inline .select2-search__field {
    margin-top: .45rem; height: 1.6rem; font-size: .875rem; font-family: inherit; color: #0f1e3a;
}
.select2-container--default .select2-selection--multiple .select2-search__field::placeholder { color: #8a93a6; }
.select2-container--default.select2-container--disabled .select2-selection--multiple { background: #f2f3f6; }

.select2-dropdown { border-color: #dbdde3; }
.select2-container--default .select2-results__option--highlighted[aria-selected] { background: #265ea9; }
.select2-container--default .select2-results__option[aria-selected=true] { background: #dde7f4; color: #1e4d8a; }
.select2-search--dropdown .select2-search__field { border: 1px solid #dbdde3; border-radius: .375rem; }

/* Plain tables (dashboard previews, empty states) — inset from the card edge too.
   DataTables tables sit inside .dataTables_wrapper, so this only hits non-DT tables. */
.table-wrap:has(> table.table) { padding: .3rem 1.25rem .9rem; }

/* ----------------------------------------------------------- DataTables */
/* Inset the whole block (search + table + pagination) so it isn't flush with the card. */
.dataTables_wrapper { padding: .6rem 1.25rem .9rem; }
.dataTables_wrapper .dataTables_filter { padding: .1rem 0 .8rem; }
.dataTables_wrapper .dataTables_filter input {
    border: 1px solid #dbdde3; border-radius: .5rem; padding: .4rem .7rem; margin-left: .4rem; outline: none;
}
.dataTables_wrapper .dataTables_filter input:focus { border-color: #265ea9; box-shadow: 0 0 0 2px rgba(38, 94, 169, .25); }
.dataTables_wrapper .dataTables_info { color: #4a5568; padding: .8rem 0 0; }
.dataTables_wrapper .dataTables_paginate { padding: .7rem 0 0; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
    border-radius: .4rem !important; padding: .3rem .7rem !important; border: 1px solid transparent !important; background: transparent !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
    background: #265ea9 !important; color: #fff !important; border-color: #265ea9 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
    background: #f2f3f6 !important; color: #0f1e3a !important; border-color: #e6e8ee !important;
}
.dataTables_wrapper .dataTables_filter, .dataTables_wrapper .dataTables_info { color: #4a5568; }
/* DataTables sort icons shouldn't fight our header colours */
table.dataTable thead th { position: relative; }

/* -------------------------------------------------- Collapsible nav groups */
.nav-group-toggle { background: none; border: 0; cursor: pointer; }
.nav-group-toggle:hover .nav-group-chevron { color: #4a5568; }
.nav-group-chevron { transition: transform .2s ease; }
.nav-group.is-collapsed .nav-group-chevron { transform: rotate(-90deg); }
/* Smooth height fold via animatable grid rows; collapses even without transition support. */
.nav-group-items { display: grid; grid-template-rows: 1fr; transition: grid-template-rows .2s ease; }
.nav-group.is-collapsed .nav-group-items { grid-template-rows: 0fr; }
.nav-group-items-inner { overflow: hidden; min-height: 0; }

/* --------------------------------------------------------- Custom sidebar colour */
/* Enabled by data-sidebar-custom on <html>; the --sb-* vars are emitted per-request
   in header.php from the admin's chosen colour. #id selectors outrank the sidebar's
   Tailwind utilities so the custom colour always wins.
   Active links carry .bg-brand-50; everything else is an inactive link. */
html[data-sidebar-custom] #sidebar {
    background-color: var(--sb-bg);
    border-right-color: var(--sb-border);
    color: var(--sb-fg-muted);
}
html[data-sidebar-custom] #sidebar .border-b { border-bottom-color: var(--sb-border); }
html[data-sidebar-custom] #sidebar .leading-tight > div:first-child { color: var(--sb-fg); }
html[data-sidebar-custom] #sidebar .leading-tight > div:last-child,
html[data-sidebar-custom] #sidebar .nav-group-toggle span,
html[data-sidebar-custom] #sidebar .nav-group-chevron { color: var(--sb-fg-muted); }
html[data-sidebar-custom] #sidebar a { color: var(--sb-fg-muted); }
html[data-sidebar-custom] #sidebar a:not(.bg-brand-50):hover {
    background-color: var(--sb-hover-bg);
    color: var(--sb-fg);
}
html[data-sidebar-custom] #sidebar a.bg-brand-50 {
    background-color: var(--sb-active-bg);
    color: var(--sb-fg);
}

/* ------------------------------------ Topbar background (mobile = page bg) */
/* On mobile the topbar blends into the page gradient; on desktop it gets the
   frosted-glass treatment so content scrolling underneath stays readable. */
.app-topbar {
    background: linear-gradient(155deg, #f7f8fa 0%, #f2f3f6 45%, #e9ebf1 100%) fixed;
}
@media (min-width: 1024px) {
    .app-topbar {
        /* background: rgb(255 255 255 / 0.9); */
        backdrop-filter: blur(8px);
        -webkit-backdrop-filter: blur(8px);
    }
}

/* ------------------------------------------------ Desktop sidebar collapse */
/* PC only — the sidebar shrinks to an icon-only mini rail (AdminLTE style)
   instead of disappearing, and the content pane follows its width. Mobile
   (< lg) is untouched: there the sidebar stays hidden off-canvas and opens as
   a slide-in overlay, so this class has no effect below the breakpoint. */
:root { --sb-width: 16rem; --sb-width-mini: 4.5rem; }
.app-main { transition: padding-left .2s ease; }
#sidebar { transition: transform .2s ease, width .2s ease; }
#sidebar .sidebar-logo-mini { display: none; }

@media (min-width: 1024px) {
    html.sidebar-collapsed #sidebar { width: var(--sb-width-mini); }
    html.sidebar-collapsed .app-main { padding-left: var(--sb-width-mini); }

    /* Brand: the wide company logo swaps for the square minimize mark. */
    html.sidebar-collapsed #sidebar .sidebar-brand { padding-left: .5rem; padding-right: .5rem; }
    html.sidebar-collapsed #sidebar .sidebar-logo-full { display: none; }
    html.sidebar-collapsed #sidebar .sidebar-logo-mini { display: block; }

    /* Nav: icons only, centred. Labels fold away and the badge becomes a small
       corner pill on the icon itself. */
    html.sidebar-collapsed #sidebar-nav { padding-left: .625rem; padding-right: .625rem; }
    html.sidebar-collapsed #sidebar-nav a { position: relative; justify-content: center; padding-left: .5rem; padding-right: .5rem; }
    html.sidebar-collapsed #sidebar-nav .nav-label { display: none; }
    html.sidebar-collapsed #sidebar-nav .badge-count {
        position: absolute; top: .15rem; right: .25rem; margin-left: 0;
        min-width: 1rem; height: 1rem; padding: 0 .2rem;
        font-size: 9px; line-height: 1rem;
    }

    /* Section headers make no sense without labels: hide them, keep a divider,
       and force every group open so no icon hides inside a folded group. */
    html.sidebar-collapsed #sidebar .nav-group-toggle { display: none; }
    html.sidebar-collapsed #sidebar .nav-group { margin-top: .5rem; padding-top: .5rem; border-top: 1px solid var(--sb-border, #e6e8ee); }
    html.sidebar-collapsed #sidebar .nav-group .nav-group-items { grid-template-rows: 1fr; }
}

/* --------------------------------------------------------- Page fade-in */
/* Content fades/slides in on every page load. Respects reduced-motion.
   Fill mode is `backwards`, not `both`: a transform animation that keeps
   filling after it ends makes <main> the containing block for every
   position:fixed descendant, so modals (calendar day sheet etc.) anchored to
   <main> instead of the viewport — pushed down by the topbar height, their
   bottom off-screen on mobile and unreachable with the body scroll-locked. */
@media (prefers-reduced-motion: no-preference) {
    .app-main > main { animation: page-fade .35s ease backwards; }
}
@keyframes page-fade {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}

/* ------------------------------------------------------------------ Print */
@media print {
    .no-print, #sidebar, header.sticky, footer { display: none !important; }
    .dataTables_filter, .dataTables_info, .dataTables_paginate, .dataTables_length { display: none !important; }
    body { background: #fff !important; color: #000 !important; }
    .lg\:pl-64 { padding-left: 0 !important; }
    .print-area { box-shadow: none !important; border: none !important; }
    main { padding: 0 !important; }
}

/* ----------------------------------------------- Rendered markdown (.md-body)
   Blogger-style typography for portfolio post bodies. This is raw HTML output
   from Parsedown (sanitized server-side). */
.md-body { font-size: 1.05rem; line-height: 1.8; color: #1a2438; word-wrap: break-word; }
.md-body > :first-child { margin-top: 0; }
.md-body h1 { font-size: 1.75rem; font-weight: 700; margin: 2rem 0 .8rem; color: #0f1e3a; }
.md-body h2 { font-size: 1.5rem; font-weight: 700; margin: 2rem 0 .8rem; color: #0f1e3a; }
.md-body h3 { font-size: 1.2rem; font-weight: 700; margin: 1.6rem 0 .6rem; color: #0f1e3a; }
.md-body h4 { font-size: 1.05rem; font-weight: 700; margin: 1.2rem 0 .5rem; color: #0f1e3a; }
.md-body p  { margin: 0 0 1rem; }
.md-body ul, .md-body ol { margin: 0 0 1rem 1.4rem; }
.md-body ul { list-style: disc; }
.md-body ol { list-style: decimal; }
.md-body li { margin: .25rem 0; }
.md-body blockquote {
    border-left: 4px solid #265ea9; background: #eef3fa; color: #1e4d8a;
    padding: .6rem 1rem; border-radius: .5rem; margin: 0 0 1rem;
}
.md-body a { color: #265ea9; text-decoration: underline; }
.md-body a:hover { color: #1e4d8a; }
.md-body code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em;
    background: #f2f3f6; padding: .12rem .4rem; border-radius: .35rem;
}
.md-body pre { background: #0f1e3a; color: #e6e8ee; padding: 1rem; border-radius: .65rem; overflow-x: auto; margin: 0 0 1rem; }
.md-body pre code { background: none; padding: 0; color: inherit; font-size: .85rem; }
.md-body img { max-width: 100%; height: auto; border-radius: .65rem; margin: 0 0 1rem; }
/* Inline video players (YouTube/Vimeo iframe or direct <video>) — responsive 16:9 */
.md-body .md-embed { position: relative; width: 100%; aspect-ratio: 16 / 9; margin: 0 0 1.25rem; border-radius: .65rem; overflow: hidden; background: #000; }
.md-body .md-embed iframe, .md-body .md-embed video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.md-body .md-embed-video video { object-fit: contain; }
.md-body hr { border: 0; border-top: 1px solid #e6e8ee; margin: 1.5rem 0; }
.md-body table { width: 100%; border-collapse: collapse; margin: 0 0 1rem; }
.md-body th, .md-body td { border: 1px solid #e6e8ee; padding: .5rem .7rem; text-align: left; }
.md-body th { background: #f7f8fa; font-weight: 600; }


/* ------------------------------------------- EasyMDE editor (pro post form)
   Fit the vendored editor into the app's input/card look. */
.EasyMDEContainer { max-width: 100%; }
.EasyMDEContainer .editor-toolbar { border-color: #e6e8ee; background: #f7f8fa; border-radius: .65rem .65rem 0 0; }
.EasyMDEContainer .CodeMirror { border-color: #e6e8ee; border-radius: 0 0 .65rem .65rem; background: #fff; color: #0f1e3a; }
.EasyMDEContainer .editor-preview { background: #fff; padding: 1rem; }

/* ------------------------------------------------------- Corner status ribbon
   Folded-paper ribbon pinned to a card's top-right corner (bay picker: busy /
   inactive). The outer box clips to a small square so the rotated strip reads
   as "wrapping" the corner; the ::before/::after triangles are a darker shade
   of the same colour to fake the paper fold where the strip exits the square. */
.corner-ribbon { position: absolute; top: -2px; right: -2px; width: 90px; height: 90px; overflow: hidden; z-index: 10; pointer-events: none; }
.corner-ribbon::before, .corner-ribbon::after { position: absolute; z-index: -1; content: ""; display: block; border: 3px solid; }
.corner-ribbon::before { top: 0; left: 3px; }
.corner-ribbon::after  { bottom: 3px; right: 0; }
.corner-ribbon span {
    position: absolute; display: block; width: 130px; padding: 8px 0; left: -15px; top: 18px;
    transform: rotate(45deg); text-align: center; font-size: 11px; font-weight: 700; line-height: 1.2;
    color: #fff; box-shadow: 0 3px 6px rgba(15, 23, 42, .25); text-shadow: 0 1px 1px rgba(0, 0, 0, .15);
}
.corner-ribbon.is-inactive::before, .corner-ribbon.is-inactive::after { border-color: #4a5568; }
.corner-ribbon.is-inactive span { background: #4a5568; }
.corner-ribbon.is-busy::before, .corner-ribbon.is-busy::after { border-color: #b45309; }
.corner-ribbon.is-busy span { background: #d97706; }
.corner-ribbon.is-free::before, .corner-ribbon.is-free::after { border-color: #1e4d8a; }
.corner-ribbon.is-free span { background: #265ea9; }

/* ------------------------------------------------ Icon picker (menu manager)
   A radio group rendered as selectable tiles. Written by hand because the
   pre-built tailwind.css ships only the utilities already used in the app —
   peer-checked/sr-only aren't in it. */
.icon-pick { display: inline-flex; cursor: pointer; }
.icon-pick input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.icon-pick > span {
    display: flex; align-items: center; justify-content: center;
    width: 2.75rem; height: 2.75rem; border-radius: .5rem;
    border: 1px solid #e6e8ee; color: #4a5568; transition: all .15s ease;
}
.icon-pick > span svg { width: 1.25rem; height: 1.25rem; }
.icon-pick:hover > span { border-color: #dbdde3; color: #3d465c; }
.icon-pick input:checked + span { border-color: #265ea9; background: #eef3fa; color: #1e4d8a; }
.icon-pick input:focus-visible + span { outline: 2px solid #265ea9; outline-offset: 2px; }

/* ------------------------------------- Helper link parked on a field's label row
   ("ลืมรหัสผ่าน?" next to the password label). The link is written *after* the
   input in the DOM so keyboard tab order runs field → field; this pins it back
   to the right end of the label line. Needs a positioned wrapper (.relative).
   line-height matches the .label line box so the smaller text sits centred. */
.label-link { position: absolute; right: 0; top: 0; line-height: 1.2rem; }

/* --------------------------------------------- Promo banner bounce (my_dashboard)
   The "คอร์สพิเศษ" strip nudges itself every few seconds so it reads as the one
   thing on the page asking to be clicked. Most of the cycle is idle — the hop
   only occupies the last ~25% — and hovering parks it so the link stays an easy
   target. Motion is dropped entirely under prefers-reduced-motion. */
.promo-bounce { will-change: transform; }
.promo-bounce:hover { animation-play-state: paused; }
.promo-bounce:hover .promo-arrow { animation-play-state: paused; }

@media (prefers-reduced-motion: no-preference) {
    .promo-bounce { animation: promo-bounce 2.8s cubic-bezier(.28, .84, .42, 1) infinite; }
    .promo-arrow  { display: inline-block; animation: promo-arrow 2.8s ease-in-out infinite; }
}

@keyframes promo-bounce {
    0%, 74%, 100% { transform: translateY(0)     scale(1); }
    80%           { transform: translateY(-6px)  scale(1.012); }
    86%           { transform: translateY(0)     scale(1); }
    91%           { transform: translateY(-2.5px) scale(1.004); }
    96%           { transform: translateY(0)     scale(1); }
}
@keyframes promo-arrow {
    0%, 74%, 100% { transform: translateX(0); }
    82%           { transform: translateX(4px); }
    92%           { transform: translateX(0); }
}

/* ------------------------------------------------- Segmented tab switcher
   Pill switch used to flip between content sets on one page (shop: private vs
   group courses). Hand-written for the same reason as .icon-pick above — the
   pre-built tailwind.css only ships utilities already used in the app. */
.seg {
    display: inline-flex; gap: .25rem; padding: .3rem;
    background: #fff; border: 1px solid #e6e8ee; border-radius: 1rem;
    box-shadow: 0 1px 2px rgba(21, 42, 80, .06);
}
.seg-btn {
    display: inline-flex; align-items: center; gap: .45rem;
    border: 0; background: transparent; cursor: pointer;
    padding: .55rem 1.5rem; border-radius: .8rem; white-space: nowrap;
    font-size: .875rem; font-weight: 700; letter-spacing: .04em;
    color: var(--ink-700); transition: background .15s, color .15s;
}
.seg-btn:hover { background: #f7f8fa; }
.seg-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.seg-btn[aria-selected="true"] { background: #c6d9ef; color: var(--ink-700); }
.seg-count { font-size: .75rem; font-weight: 600; opacity: .65; }
.seg-count.is-pop { display: inline-block; }
/* Narrow screens: scroll the switch instead of squeezing its tabs. Four Thai
   labels plus their counts (my_bookings) can't share 375px legibly. */
@media (max-width: 640px) {
    .seg {
        width: 100%; flex-wrap: nowrap; overflow-x: auto;
        scrollbar-width: none; -webkit-overflow-scrolling: touch;
    }
    .seg::-webkit-scrollbar { display: none; }
    .seg-btn { flex: 0 0 auto; justify-content: center; padding-left: .9rem; padding-right: .9rem; }
}

/* =============================================== Shop / buy page (customer)
   The buy page is the one screen that has to *sell* something, so it gets a
   slower, more deliberate motion language than the rest of the app: a
   staggered entrance, a gold hairline that draws itself across the top edge on
   hover, one sheen pass over the card, and a seat meter that fills itself.

   Rules of the block: every animation moves transform/opacity only (plus the
   one width keyframe on the seat meter, which is 6px tall), nothing loops fast
   enough to nag, and the whole thing is inert under prefers-reduced-motion —
   the final/resting state of every element is the plain non-animated CSS, so
   dropping the motion never hides content. Hand-written for the same reason as
   .seg above: the pre-built tailwind.css only ships utilities already in use. */

/* Sort chips under the type tabs. Toggle buttons, not a <select>: one tap, and
   the pressed one stays visible so the current order is never a mystery. */
.sortbar { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; margin: -.6rem 0 1.15rem; }
.sortbar-label {
    margin-right: .2rem; font-size: .7rem; font-weight: 600;
    letter-spacing: .07em; text-transform: uppercase; color: var(--ink-400);
}
.sort-chip, .sort-clear {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .35rem .75rem; border-radius: 999px; cursor: pointer;
    font-size: .76rem; font-weight: 600; white-space: nowrap;
    background: #fff; border: 1px solid #e6e8ee; color: var(--ink-600);
    transition: transform .25s cubic-bezier(.22, .8, .28, 1), background .2s ease,
                border-color .2s ease, box-shadow .25s ease, color .2s ease;
}
.sort-chip:hover, .sort-clear:not([disabled]):hover {
    transform: translateY(-2px); border-color: #c6d9ef;
    box-shadow: 0 8px 16px -10px rgba(18, 51, 31, .45);
}
.sort-chip:focus-visible, .sort-clear:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }
.sort-chip[aria-pressed="true"] {
    background: linear-gradient(120deg, #dde7f4, #9fbde0);
    border-color: #1e4d8a; color: #0f1e3a;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5);
}
/* The "เกี่ยวกับคอร์ส" chip is an invitation, not a filter: gold fill, a slow
   glint and a breathing ring so it reads as the thing to open before picking a
   course. Kept on the .sort-chip base so it still lines up with the filters. */
.chip-about {
    position: relative; overflow: hidden; font-weight: 700;
    background: linear-gradient(120deg, #eef3fa, #9fbde0 55%, #1e4d8a);
    border-color: #1e4d8a; color: #0f1e3a;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .55);
}
.chip-about:hover { transform: translateY(-2px) scale(1.02); border-color: #1e4d8a; }
.chip-about-hint { font-weight: 600; opacity: .72; }
/* The glint sweep — a light band that crosses the pill now and then. */
.chip-about::after {
    content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .7), transparent);
    transform: translateX(-140%); pointer-events: none;
}

@media (max-width: 400px) { .chip-about-hint { display: none; } }

@media (prefers-reduced-motion: no-preference) {
    .chip-about          { animation: about-pulse 3.4s ease-in-out infinite; }
    .chip-about:hover    { animation: none; box-shadow: 0 10px 20px -10px rgba(18, 51, 31, .55), inset 0 1px 0 rgba(255, 255, 255, .55); }
    .chip-about::after   { animation: shop-glint 4.6s ease-in-out 1.2s infinite; }
}

/* Ring that grows out of the pill and fades — attention without movement. */
@keyframes about-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(38, 94, 169, .45), inset 0 1px 0 rgba(255, 255, 255, .55); }
    55%      { box-shadow: 0 0 0 7px rgba(38, 94, 169, 0),  inset 0 1px 0 rgba(255, 255, 255, .55); }
}

.sort-clear { border-style: dashed; color: var(--ink-500); }
.sort-clear[disabled] { opacity: .45; cursor: not-allowed; }

/* Section heading above a panel's grid. Earns its keep on the "ทั้งหมด" tab,
   where two panels stack and each has to say what it is. */
.shop-section {
    display: flex; align-items: center; gap: .55rem;
    margin: 0 0 .9rem; font-size: .78rem; font-weight: 700;
    letter-spacing: .09em; text-transform: uppercase; color: var(--ink-500);
}
.shop-section::after { content: ''; flex: 1 1 auto; height: 1px; background: linear-gradient(90deg, #9fbde0, rgba(159, 189, 224, 0)); }
.shop-lead { margin: -.35rem 0 1rem; font-size: .85rem; color: var(--ink-500); }
.shop-panel + .shop-panel { margin-top: 1.9rem; }

/* Own grid so the entrance stagger reads left-to-right at every width. */
.shop-grid { display: grid; gap: 1.15rem; grid-template-columns: 1fr; align-items: stretch; }
@media (min-width: 768px)  { .shop-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .shop-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* The shop's own grids run denser than the shared one: two cards side by side on
   a phone (the gap tightens so each still has usable width) up to four across on
   a desktop. Scoped to .is-quad so my_courses / my_bookings / portfolios, whose
   cards carry longer detail rows, keep the roomier default. */
.shop-grid.is-quad { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .7rem; }
@media (min-width: 640px)  { .shop-grid.is-quad { gap: 1.15rem; } }
@media (min-width: 1024px) { .shop-grid.is-quad { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1280px) { .shop-grid.is-quad { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Private-course listing (shop.php step 2) reads better as one card per row —
   the cards carry a price + payment-method row that gets cramped at is-quad's
   density. Scoped to .is-single so the pro-picker grid (same .is-quad) is untouched. */
.shop-grid.is-quad.is-single { grid-template-columns: 1fr !important; gap: 1.15rem; }

/* Two cards on a phone means ~160px of card, so the card's own typography has to
   come down with it — otherwise the title eats four lines and the "เรียนกับโปร
   ท่านนี้แล้ว N คน" ribbon (nowrap by design, it is a tapered banner) runs past
   the card edge and gets clipped. */
@media (max-width: 639px) {
    .shop-grid.is-quad:not(.is-single) .shop-body    { padding: .85rem .9rem .9rem; }
    .shop-grid.is-quad:not(.is-single) .shop-eyebrow { font-size: .62rem; letter-spacing: .04em; }
    .shop-grid.is-quad:not(.is-single) .shop-title   { font-size: .95rem; }
    .shop-grid.is-quad:not(.is-single) .shop-price   { font-size: 1.35rem; }
    .shop-grid.is-quad:not(.is-single) .pro-pick-go  { font-size: .72rem; padding-top: .7rem; }
    .shop-grid.is-quad:not(.is-single) .shop-ribbon {
        white-space: normal; font-size: .64rem;
        margin-left: -.9rem; padding: .3rem 1.05rem .3rem .9rem;
    }
    .shop-grid.is-quad:not(.is-single) .shop-ribbon b { font-size: .7rem; }
}

/* The wrapper carries the entrance animation and the card carries the hover
   motion — an animation's fill state would otherwise outrank :hover's
   transform and the card would refuse to lift. */
.shop-reveal { display: flex; }
.shop-reveal > .shop-card { flex: 1 1 auto; }
/* app.css loads after tailwind.css, so the utility's display:none would lose to
   the rule above on source order — restate it for the search filter. */
.shop-reveal.hidden { display: none; }

.shop-card {
    position: relative; overflow: hidden; display: flex; flex-direction: column;
    transition: transform .45s cubic-bezier(.22, .8, .28, 1),
                box-shadow .45s cubic-bezier(.22, .8, .28, 1),
                border-color .3s ease;
}
/* Lift on hover only — :focus-within would jog the card while the customer is
   tabbing through its own form fields. The hairline still answers focus. */
.shop-card:hover {
    transform: translateY(-5px); border-color: #c6d9ef;
    box-shadow: 0 22px 45px -22px rgba(18, 51, 31, .35), 0 2px 6px rgba(18, 51, 31, .06);
}
/* Gold hairline, drawn left → right on hover. */
.shop-card::before {
    content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; z-index: 1;
    background: linear-gradient(90deg, var(--accent-dark), #dde7f4 45%, var(--accent));
    transform: scaleX(0); transform-origin: left center;
    transition: transform .55s cubic-bezier(.22, .8, .28, 1);
}
.shop-card:hover::before, .shop-card:focus-within::before { transform: scaleX(1); }
/* Sheen band — parked off-card and swept across once per hover. */
.shop-card::after, .btn-shine::after {
    content: ''; position: absolute; top: -25%; bottom: -25%; left: -45%; width: 35%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .7), transparent);
    transform: skewX(-16deg); opacity: 0; pointer-events: none;
}
.shop-card.is-special { border-color: #9fbde0; box-shadow: 0 10px 30px -18px rgba(169, 126, 56, .5); }
.shop-card.is-group   { border-left: 3px solid #9fbde0; }
.shop-card.is-next    { border-left: 3px solid var(--accent); }   /* booking still ahead */
.shop-card.is-off     { opacity: .72; }                          /* cancelled / no-show */
.shop-card.is-off:hover { opacity: 1; }

/* Cover image on a card that has no padding of its own (portfolios). */
.shop-cover {
    display: block; overflow: hidden; aspect-ratio: 16 / 9;
    background: linear-gradient(135deg, #eef3fa, #dde7f4);
}
.shop-cover img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .7s cubic-bezier(.22, .8, .28, 1); }
.shop-card:hover .shop-cover img { transform: scale(1.06); }
.shop-cover-empty { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: #c6d9ef; }
.shop-cover-empty svg { width: 3rem; height: 3rem; }
.shop-body { padding: 1.15rem 1.25rem 1.25rem; display: flex; flex-direction: column; flex: 1 1 auto; }
.shop-excerpt { margin-top: .4rem; font-size: .85rem; color: var(--ink-500); flex: 1 1 auto; }
.shop-byline {
    margin-top: .9rem; padding-top: .8rem; border-top: 1px solid #f2f3f6;
    display: flex; align-items: center; gap: .5rem; font-size: .8rem; color: var(--ink-500);
}
.shop-byline a { color: inherit; text-decoration: none; transition: color .2s ease; }
.shop-byline a:hover, .shop-title a:hover { color: var(--ink-800); }
.shop-title a { color: inherit; text-decoration: none; }

/* Card typography */
.shop-eyebrow { font-size: .7rem; font-weight: 600; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-400); }
.shop-title   { margin-top: .35rem; font-size: 1.0625rem; font-weight: 700; line-height: 1.35; color: var(--ink-900); }
.shop-when    { margin-top: .3rem; font-size: .8rem; color: var(--ink-600); }
.shop-price   { font-size: 1.75rem; font-weight: 800; line-height: 1.1; letter-spacing: -.02em; color: var(--ink-700); font-variant-numeric: tabular-nums; }
.shop-unit    { font-size: .8rem; color: var(--ink-400); }
.shop-meta    { margin-top: .15rem; font-size: .72rem; color: var(--ink-400); }

/* "คอร์สพิเศษ" pill — gold plate with a slow, occasional glint. */
.shop-badge {
    position: relative; overflow: hidden; flex: none;
    display: inline-flex; align-items: center; gap: .25rem;
    padding: .18rem .55rem; border-radius: 999px;
    font-size: .68rem; font-weight: 700; letter-spacing: .02em; color: #152a50;
    background: linear-gradient(120deg, #dde7f4, #9fbde0);
    box-shadow: 0 1px 2px rgba(110, 81, 36, .3);
}
.shop-badge::after {
    content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 45%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .9), transparent);
    transform: translateX(-140%);
}

/* Social proof — "เรียนคอร์สนี้แล้ว N คน" as a gold ribbon.
   The negative left margin equals .card-p's padding, so the banner runs out to
   the card's own edge and the card's overflow:hidden trims it flush — that flush
   cut is what makes it read as a ribbon rather than a chip. The right end is
   notched with clip-path (which also clips any outer box-shadow, hence the inset
   highlight/shade doing the lift instead). Hidden at zero learners (shop.php). */
.shop-ribbon {
    position: relative; align-self: flex-start; overflow: hidden;
    margin: .7rem 0 .1rem -1.25rem;
    padding: .32rem 1.5rem .32rem 1.25rem;
    display: inline-flex; align-items: center; gap: .4rem;
    font-size: .74rem; font-weight: 700; color: #0f1e3a; white-space: nowrap;
    background: linear-gradient(100deg, #1e4d8a 0%, #265ea9 30%, #dde7f4 52%, #265ea9 74%, #1e4d8a 100%);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5), inset 0 -1px 0 rgba(110, 81, 36, .35);
    clip-path: polygon(0 0, 100% 0, calc(100% - .6rem) 50%, 100% 100%, 0 100%);
}
.shop-ribbon b { font-weight: 800; font-size: .82rem; font-variant-numeric: tabular-nums; }
.shop-ribbon svg { flex: none; width: .9rem; height: .9rem; opacity: .8; }
/* Glint band, parked off-ribbon between passes. */
.shop-ribbon::after {
    content: ''; position: absolute; top: 0; bottom: 0; left: 0; width: 40%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .85), transparent);
    transform: translateX(-140%); pointer-events: none;
}

/* Payment-channel chips + their frame */
.shop-paybox { border-radius: .8rem; padding: .6rem .7rem; background: linear-gradient(135deg, #f7f8fa, #f2f3f6); border: 1px solid #e6e8ee; }
.shop-chipline { display: flex; flex-wrap: wrap; gap: .4rem; }
.pay-chip {
    display: inline-flex; align-items: center; gap: .35rem;
    padding: .3rem .55rem; border-radius: .6rem; background: #fff; border: 1px solid #f2f3f6;
    font-size: .72rem; font-weight: 600; color: var(--ink-600);
    transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease;
}
.pay-chip:hover { transform: translateY(-2px); border-color: #c6d9ef; box-shadow: 0 6px 14px -8px rgba(21, 42, 80, .35); }
.shop-note { margin-top: .45rem; font-size: .72rem; color: var(--ink-500); }
/* Semantic warning — stays amber rather than following the accent, so "ใกล้เต็ม /
   ต้องระวัง" still reads as a warning at a glance. */
.shop-warn { border-radius: .8rem; padding: .6rem .7rem; background: #fdf0d8; border: 1px solid #f3ddb0; font-size: .8rem; color: #9a6b13; }

/* Seat meter (group classes) */
.shop-seatrow { margin-top: .5rem; display: flex; align-items: center; gap: .6rem; }
.shop-seat { flex: 1 1 auto; height: 6px; border-radius: 999px; background: #f2f3f6; overflow: hidden; }
.shop-seat > i { display: block; height: 100%; width: var(--w, 0%); border-radius: 999px; background: linear-gradient(90deg, #5b8cc8, #265ea9); }
.shop-seat.is-low > i { background: linear-gradient(90deg, #f3ddb0, #c08a1b); }
.shop-seatnum { flex: none; font-size: .72rem; font-weight: 600; color: var(--ink-500); font-variant-numeric: tabular-nums; }
.shop-seatnum.is-low { color: #9a6b13; }

/* Status pills (my_courses). Palette-native replacements for the slate/violet
   tailwind pills these cards used to carry. */
.shop-pill {
    display: inline-flex; align-items: center; gap: .3rem; white-space: nowrap;
    padding: .18rem .6rem; border-radius: 999px; font-size: .68rem; font-weight: 700;
}
.shop-pill.is-done  { background: #e1f0e6; color: #2f7449; }   /* finished — semantic success */
.shop-pill.is-live  { background: #eef3fa; color: #265ea9; }   /* in progress — accent */
.shop-pill.is-wait  { background: #fdf0d8; color: #9a6b13; }   /* waiting — semantic warning */
.shop-pill.is-group { background: #dde7f4; color: #1e4d8a; }

/* Secondary detail block inside a card (class date/time, notes). */
.shop-infobox {
    margin-top: .7rem; padding: .6rem .7rem; border-radius: .8rem;
    background: linear-gradient(135deg, #f7f8fa, #f2f3f6); border: 1px solid #e6e8ee;
    font-size: .8rem; color: var(--ink-600);
}
.shop-alert { margin-top: .35rem; font-weight: 600; color: #b91c1c; }

/* Buy form pinned to the bottom so cards of unequal height still line up. */
.shop-form { margin-top: auto; padding-top: .9rem; border-top: 1px solid #f2f3f6; display: grid; gap: .5rem; }
/* Same idea for a row of action buttons instead of a form. */
.shop-actions {
    margin-top: auto; padding-top: .9rem; border-top: 1px solid #f2f3f6;
    display: flex; flex-wrap: wrap; gap: .5rem;
}
.shop-consent { display: flex; align-items: flex-start; gap: .5rem; font-size: .72rem; color: var(--ink-500); }
.shop-consent input { margin-top: .15rem; flex: none; }

/* Instructor picker — step 1 of buying a private course (shop.php). The card
   carries two links (pick this pro / read their work), so the picker is its own
   block filling the card rather than an anchor wrapped around the whole thing. */
.pro-pick-main {
    display: flex; flex-direction: column; flex: 1 1 auto;
    color: inherit; text-decoration: none;
}
.pro-pick-main:hover { color: inherit; }

/* No hairline frame on these cards: the photo is full-bleed to the rounded
   corners, and .card's pale border read as a white edge boxing the image in.
   The shadow (and the gold ::before on hover) still separates card from page. */
.shop-card.pro-pick, .shop-card.pro-pick:hover { border: 0; }

/* The pro's photo fills the top of the card — it's the thing being chosen, so it
   gets the room. 16/9 matches the "Meet Pro <name>" key art these photos are cut
   from, so the whole frame (the pro's name is printed *inside* the image) is on
   screen edge to edge: any other ratio would crop the name off the sides, and
   object-fit: contain would letterbox it in bars instead. For the same reason
   there is no hover zoom here — 1.06 would eat the lettering again. */
.pro-photo {
    position: relative; overflow: hidden; aspect-ratio: 16 / 9;
    background: linear-gradient(135deg, #eef3fa, #dde7f4);
}
.pro-photo img {
    width: 100%; height: 100%; object-fit: cover; object-position: center; display: block;
}
/* No photo yet: the initial on the same gold wash as the small avatars. */
.pro-photo-empty {
    display: flex; align-items: center; justify-content: center; height: 100%;
    background: #dde7f4; color: #152a50; font-size: 3.5rem; font-weight: 700; line-height: 1;
}
/* Age pills sit on the photo, over a scrim so they stay legible on any image. */
.pro-photo-tags {
    position: absolute; inset: auto .8rem .7rem; z-index: 1;
    display: flex; flex-wrap: wrap; gap: .3rem;
}
.pro-photo::after {
    content: ''; position: absolute; inset: 45% 0 0; pointer-events: none;
    background: linear-gradient(180deg, rgba(11, 36, 22, 0), rgba(11, 36, 22, .45));
}
/* Call to action pinned to the bottom edge, matching .shop-form's divider so a
   pro card and a course card line up in the same grid row. */
.pro-pick-go {
    margin-top: auto; padding-top: .9rem; border-top: 1px solid #f2f3f6;
    font-size: .78rem; font-weight: 700; color: var(--ink-600);
}
.pro-pick-main:hover .pro-pick-go { color: var(--ink-800); }

/* The pro's published work — a quieter strip under the picker, so the card reads
   as one primary action (choose this pro) with a secondary one below it. */
.pro-pick-work {
    display: flex; align-items: center; justify-content: center; gap: .4rem;
    padding: .65rem 1.25rem; border-top: 1px solid #f2f3f6; background: #fbfcfd;
    font-size: .76rem; font-weight: 600; color: var(--ink-500); text-decoration: none;
    transition: background .2s ease, color .2s ease;
}
.pro-pick-work:hover { background: #eef3fa; color: var(--ink-800); }
.pro-pick-work svg { width: .95rem; height: .95rem; flex: none; opacity: .8; }

/* The chosen-pro strip above the course grid: who was picked, the age dropdown
   and the way back. Stacks on a phone, one row from tablet up. */
.pro-bar { display: flex; flex-direction: column; gap: .9rem; }
.pro-bar-age { display: grid; gap: .25rem; }
@media (min-width: 640px) {
    .pro-bar { flex-direction: row; align-items: center; justify-content: space-between; flex-wrap: wrap; }
    .pro-bar-age { margin-left: auto; }
}

/* "โหลดเพิ่ม" footer under a folded grid. Same source-order caveat as
   .shop-reveal.hidden above — the display rule has to be undone explicitly. */
.shop-more { display: flex; justify-content: center; margin-top: 1.15rem; }
.shop-more.hidden { display: none; }
.shop-more > .btn { min-width: 13rem; border-color: #c6d9ef; }
.shop-more > .btn:hover { background: #f7f8fa; }

/* Points banner — the one permanently animated element on the page. */
.shop-points {
    position: relative; overflow: hidden;
    background: linear-gradient(135deg, #f7f8fa 0%, #dde7f4 55%, #c6d9ef 100%);
    border-color: #c6d9ef;
}
.shop-points::after {
    content: ''; position: absolute; top: -30%; bottom: -30%; left: -30%; width: 30%;
    background: linear-gradient(100deg, transparent, rgba(255, 255, 255, .75), transparent);
    transform: skewX(-14deg); opacity: 0; pointer-events: none;
}
.shop-points-icon { font-size: 1.6rem; line-height: 1; display: inline-block; }

/* Search field with a leading icon (no Tailwind icon utilities in the build). */
.input-icon { position: relative; }
.input-icon > svg { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); width: 1rem; height: 1rem; color: var(--ink-400); pointer-events: none; }
.input-icon > .input { padding-left: 2.1rem; }

/* Shine sweep for a CTA; pairs with .btn. */
.btn-shine { position: relative; overflow: hidden; }
.btn-shine:active { transform: translateY(1px); }

@media (prefers-reduced-motion: no-preference) {
    .shop-reveal        { animation: shop-rise .62s cubic-bezier(.22, .8, .28, 1) both; animation-delay: calc(var(--i, 0) * 60ms); }
    .shop-panel         { animation: shop-panel .34s ease both; }
    .shop-card:hover::after { animation: shop-sheen 1.05s cubic-bezier(.3, .6, .3, 1); }
    .btn-shine:hover::after { animation: shop-sheen .85s ease; }
    .shop-badge::after  { animation: shop-glint 4.2s ease-in-out infinite; }
    /* Offset from the badge's cycle so the two gold surfaces never glint together. */
    .shop-ribbon::after { animation: shop-glint 5.6s ease-in-out 1.4s infinite; }
    .shop-points::after { animation: shop-points-sheen 7s ease-in-out infinite; }
    .shop-points-icon   { animation: shop-float 4.2s ease-in-out infinite; }
    .shop-seat > i      { animation: shop-seat-fill 1s cubic-bezier(.22, .8, .28, 1) both; animation-delay: calc(var(--i, 0) * 60ms + .25s); }
    .seg-count.is-pop   { animation: shop-count-pop .3s cubic-bezier(.22, .8, .28, 1); }
}

@keyframes shop-rise      { from { opacity: 0; transform: translateY(16px) scale(.99); } to { opacity: 1; transform: none; } }
@keyframes shop-panel     { from { opacity: 0; transform: translateY(6px); }            to { opacity: 1; transform: none; } }
@keyframes shop-sheen     { from { opacity: .95; transform: skewX(-16deg) translateX(0); } to { opacity: 0; transform: skewX(-16deg) translateX(520%); } }
@keyframes shop-glint     { 0%, 62%, 100% { transform: translateX(-140%); } 80% { transform: translateX(160%); } }
/* Same sweep as shop-sheen, but idle for the first half of the cycle so the
   banner glints now and then instead of having a light permanently crawling. */
@keyframes shop-points-sheen {
    0%, 55%   { transform: skewX(-14deg) translateX(0);    opacity: 0; }
    58%       { opacity: .8; }
    90%, 100% { transform: skewX(-14deg) translateX(520%); opacity: 0; }
}
@keyframes shop-float     { 0%, 100% { transform: translateY(0) rotate(-5deg); } 50% { transform: translateY(-4px) rotate(5deg); } }
@keyframes shop-seat-fill { from { width: 0; } to { width: var(--w, 0%); } }
@keyframes shop-count-pop { from { transform: scale(.65); opacity: .35; } to { transform: none; opacity: .65; } }

/* ------------------------------------------------ Frameless image sheets
   The shop's two sheets (course info, PDPA) are a single tall artwork, so they
   drop the white card: only the image is on screen and the controls float over
   it. The scroller hides its scrollbar — the image itself is the scroll cue. */
.modal-sheet { position: relative; max-height: 100%; display: flex; flex-direction: column; }
.modal-sheet-scroll {
    flex: 1 1 auto; min-height: 0; overflow-y: auto;
    border-radius: 1rem; scrollbar-width: none; -ms-overflow-style: none;
    box-shadow: 0 30px 70px -30px rgba(0, 0, 0, .7);
    -webkit-overflow-scrolling: touch;
}
.modal-sheet-scroll::-webkit-scrollbar { width: 0; height: 0; }
.modal-sheet-scroll img { display: block; width: 100%; height: auto; border-radius: 1rem; }

/* Round ✕ over the top-right corner of the artwork. */
.modal-x {
    position: absolute; top: .65rem; right: .65rem; z-index: 2;
    width: 2.25rem; height: 2.25rem; border-radius: 999px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: 1rem; line-height: 1; cursor: pointer; color: #fff;
    background: rgba(11, 36, 22, .55); border: 1px solid rgba(255, 255, 255, .38);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
    transition: background .2s ease, transform .2s ease;
}
.modal-x:hover { background: rgba(11, 36, 22, .82); transform: scale(1.06); }
.modal-x:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--ring); }

/* Action button floating over the foot of the artwork instead of a footer bar. */
.modal-float-btn {
    position: absolute; left: 50%; bottom: 1rem; z-index: 2;
    transform: translateX(-50%); min-width: min(18rem, 78%);
    justify-content: center; box-shadow: 0 16px 34px -14px rgba(0, 0, 0, .7);
}
.modal-float-btn:hover { transform: translateX(-50%) translateY(-1px); }

/* ------------------------------------------- Viewport-bounded modal panel
   A panel that is pushed down from the top of the overlay (mt-14 / sm:mt-24)
   cannot also be 80vh tall: on a phone the bottom of the scroll area lands
   below the fold, and because the page body is locked with overflow-hidden
   while the modal is open there is no way to reach it. Height is capped to
   what is left under the offset instead, with dvh so the collapsing mobile
   URL bar does not eat the last rows (vh first as the fallback). */
.modal-panel-fit {
    max-height: calc(100vh - 5rem);
    max-height: calc(100dvh - 5rem);
}
@media (min-width: 640px) {
    .modal-panel-fit {
        max-height: calc(100vh - 8rem);
        max-height: calc(100dvh - 8rem);
    }
}

/* The scroller inside such a panel: momentum scrolling on iOS, and the page
   behind never takes over the gesture when the list hits its end. */
.modal-panel-scroll {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
}

/* ------------------------------------------------------- Modal open fade
   A sheet marked .modal-fade fades its backdrop in and lifts its panel when it
   becomes visible. The animation is keyed off .hidden being removed, so the
   page JS keeps toggling that one class and nothing else. */
@media (prefers-reduced-motion: no-preference) {
    .modal-fade:not(.hidden)              { animation: modal-fade .2s ease both; }
    .modal-fade:not(.hidden) .card,
    .modal-fade:not(.hidden) .modal-sheet { animation: modal-rise .28s cubic-bezier(.22, .8, .28, 1) both; }
}

@keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-rise { from { opacity: 0; transform: translateY(12px) scale(.985); } to { opacity: 1; transform: none; } }

/* ========================================= Navy text tone (Enless C.I. 2026)
   Page copy reads in the navy ink ramp (--ink-*) instead of slate. Deliberately
   NOT applied to the sidebar (#sidebar) or the topbar (.app-topbar) — the sidebar
   gets its own foreground colours from --sb-* (see sidebar_theme_vars() in
   app/core/settings.php), so every rule here is undone for their subtrees at the
   end of the section.

   This block sits last on purpose: app.css loads after the pre-built
   tailwind.css, so these plain (non-!important) rules outrank the .text-slate-*
   utilities by source order, while the #sidebar / .app-topbar restores outrank
   these by specificity. Keep it at the bottom of the file. */

/* Base copy colour (body carries .text-slate-700 from header.php). */
body { color: var(--ink-700); }

/* Tailwind neutral text utilities → ink ramp. */
.text-slate-900, .text-gray-900 { color: var(--ink-900); }
.text-slate-800, .text-gray-800 { color: var(--ink-800); }
.text-slate-700, .text-gray-700 { color: var(--ink-700); }
.text-slate-600, .text-gray-600 { color: var(--ink-600); }
.text-slate-500, .text-gray-500 { color: var(--ink-500); }
.text-slate-400, .text-gray-400 { color: var(--ink-400); }
.text-slate-300, .text-gray-300 { color: var(--ink-300); }
.hover\:text-slate-900:hover { color: var(--ink-900); }

/* Component text colours defined earlier in this file. */
.label                       { color: var(--ink-600); }
.form-hint                   { color: var(--ink-400); }
.input, .select, .textarea   { color: var(--ink-900); }
.input[readonly], .input:disabled { color: var(--ink-500); }
.input::placeholder          { color: var(--ink-400); }
.btn-secondary               { color: var(--ink-700); }
.btn-ghost                   { color: var(--ink-600); }
.table thead th              { color: var(--ink-500); }
.table tbody td              { color: var(--ink-700); }
.table tfoot td              { color: var(--ink-900); }
.dataTables_wrapper .dataTables_info,
.dataTables_wrapper .dataTables_filter { color: var(--ink-500); }
.dataTables_wrapper .dataTables_paginate .paginate_button:hover { color: var(--ink-900) !important; }
.select2-container--default .select2-selection--single .select2-selection__rendered { color: var(--ink-900); }
.select2-container--default .select2-selection--multiple .select2-search__field { color: var(--ink-900); }
.select2-container--default .select2-selection--multiple .select2-search__field::placeholder { color: var(--ink-400); }
.icon-pick > span            { color: var(--ink-500); }
.icon-pick:hover > span      { color: var(--ink-700); }
.md-body                     { color: var(--ink-700); }
.md-body h1, .md-body h2, .md-body h3, .md-body h4 { color: var(--ink-900); }
.EasyMDEContainer .CodeMirror { color: var(--ink-900); }

/* SweetAlert2 dialogs (rendered on <body>, i.e. page content, not chrome). */
.swal2-popup .swal2-title          { color: var(--ink-800); }
.swal2-popup .swal2-html-container { color: var(--ink-600); }

/* ---- Chrome exclusions: sidebar + topbar keep the original slate palette ---- */
#sidebar, .app-topbar { color: #3d465c; }
#sidebar .text-slate-900, .app-topbar .text-slate-900 { color: #0f1e3a; }
#sidebar .text-slate-800, .app-topbar .text-slate-800 { color: #1a2438; }
#sidebar .text-slate-700, .app-topbar .text-slate-700 { color: #3d465c; }
#sidebar .text-slate-600, .app-topbar .text-slate-600 { color: #4a5568; }
#sidebar .text-slate-500, .app-topbar .text-slate-500 { color: #4a5568; }
#sidebar .text-slate-400, .app-topbar .text-slate-400 { color: #8a93a6; }
#sidebar .text-slate-300, .app-topbar .text-slate-300 { color: #dbdde3; }
#sidebar .hover\:text-slate-900:hover,
.app-topbar .hover\:text-slate-900:hover { color: #0f1e3a; }
#sidebar .btn-secondary, .app-topbar .btn-secondary { color: #3d465c; }
#sidebar .btn-ghost, .app-topbar .btn-ghost { color: #4a5568; }

/* ---- PWA (public/assets/js/pwa.js): install / update / push-prompt bar ----
   One floating card at the bottom; above toastr's container (999) and the page,
   below SweetAlert (1060). Clears the iPhone home indicator via safe-area. */
.pwa-bar {
    position: fixed; z-index: 1050;
    left: .75rem; right: .75rem; bottom: calc(.75rem + env(safe-area-inset-bottom, 0px));
    max-width: 30rem; margin: 0 auto;
    display: flex; align-items: center; gap: .75rem;
    padding: .8rem 2.4rem .8rem .85rem;
    background: #fff; color: var(--ink-800);
    border: 1px solid #e6e8ee; border-radius: 1rem;
    box-shadow: 0 12px 32px rgba(15, 30, 58, .18);
    animation: pwa-bar-in .28s ease-out;
}
@keyframes pwa-bar-in { from { transform: translateY(1.25rem); opacity: 0; } to { transform: none; opacity: 1; } }
.pwa-bar--update { border-color: var(--brand); }
.pwa-bar__logo { width: 2.75rem; height: 2.75rem; flex-shrink: 0; border-radius: .7rem; border: 1px solid #eef0f4; }
.pwa-bar__icon {
    width: 2.5rem; height: 2.5rem; flex-shrink: 0; border-radius: 50%;
    display: grid; place-items: center; background: var(--brand-light); color: var(--brand);
}
.pwa-bar__icon svg { width: 1.3rem; height: 1.3rem; }
.pwa-bar__body { flex: 1; min-width: 0; }
.pwa-bar__title { font-size: .9rem; font-weight: 600; color: var(--ink-900); line-height: 1.3; }
.pwa-bar__text { font-size: .8rem; color: var(--ink-600); line-height: 1.45; margin-top: .15rem; }
.pwa-bar__note { display: block; margin-top: .2rem; font-size: .72rem; color: var(--ink-400); }
.pwa-bar__actions { display: flex; flex-direction: column; gap: .35rem; flex-shrink: 0; }
.pwa-bar__actions .btn { white-space: nowrap; justify-content: center; text-decoration: none; }
.pwa-bar__close {
    position: absolute; top: .45rem; right: .45rem; padding: .3rem; border-radius: .5rem;
    color: var(--ink-400); background: transparent; border: 0; cursor: pointer;
}
.pwa-bar__close:hover { background: #f2f3f6; color: var(--ink-700); }
.pwa-bar__close svg { width: 1rem; height: 1rem; display: block; }
.pwa-ios-share {
    display: inline-grid; place-items: center; vertical-align: -.3em;
    width: 1.45rem; height: 1.45rem; border-radius: .35rem;
    background: var(--brand-light); color: var(--brand);
}
.pwa-ios-share svg { width: 1rem; height: 1rem; }

/* "Notifications on this device" row in the bell panel (topbar.php #pushRow). */
.pwa-push-row__label { display: flex; align-items: center; gap: .6rem; min-width: 0; flex: 1; }
.pwa-push-row__label > svg { width: 1.15rem; height: 1.15rem; flex-shrink: 0; color: var(--brand); }
.pwa-push-row__name { display: block; font-size: .8rem; font-weight: 500; color: #3d465c; }
.pwa-push-row__state { display: block; font-size: .7rem; color: #8a93a6; }
.pwa-push-row__tip {
    display: flex; align-items: center; gap: .5rem; width: 100%; text-align: left;
    font-size: .78rem; color: var(--brand); background: none; border: 0; padding: 0; cursor: pointer;
}
.pwa-push-row__tip svg { width: 1.1rem; height: 1.1rem; flex-shrink: 0; }
.pwa-push-row__blocked { font-size: .75rem; color: #b45309; }
#pushRow:not(.hidden) { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.pwa-switch {
    position: relative; flex-shrink: 0; width: 2.4rem; height: 1.4rem; border-radius: 999px;
    background: #cfd3dc; border: 0; padding: 0; cursor: pointer; transition: background .2s;
}
.pwa-switch.is-on { background: var(--brand); }
.pwa-switch[disabled] { opacity: .55; cursor: progress; }
.pwa-switch__knob {
    position: absolute; top: .15rem; left: .15rem; width: 1.1rem; height: 1.1rem; border-radius: 50%;
    background: #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); transition: transform .2s;
}
.pwa-switch.is-on .pwa-switch__knob { transform: translateX(1rem); }
