/*
 * Design tokens and components.
 *
 * Direction: DESIGN.md (notion base). Warm neutral canvas, 8px control radius
 * instead of pills, and one clay accent that marks the single most important
 * action on a screen. Every text pair below was checked with the antislop
 * contrast checker; the recorded ratios are in DESIGN.md.
 */

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 400 700;
    font-display: swap;
    src: url('/assets/fonts/inter-latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+2074,
        U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
    color-scheme: light;

    /* Warm neutrals: the canvas is off-white, not grey, so the accent reads as
       ink on paper rather than as another UI colour. */
    --canvas: #fbfaf9;
    --surface: #ffffff;
    --surface-2: #f4f2f0;
    --surface-3: #ece9e5;
    --hairline: #e5e1dd;
    --hairline-strong: #cfc8c1;
    --control-border: #8e8681;

    --ink: #1f1d1b;
    --ink-muted: #5c5751;
    /* Label sekunder (judul grup menu, tanda tangan footer) tetap 4.5:1 di
       atas surface-3 yang paling gelap, bukan hanya di atas kanvas. */
    --ink-subtle: #6c665e;
    --on-accent: #ffffff;

    --accent: #b4471f;
    --accent-hover: #9c3b19;
    --accent-soft: #fbefe9;
    --focus: #b4471f;

    /* Ramp clay untuk permukaan tombol, dari clay ke terakota yang lebih pekat.
       Ujung gelap #8a2f0c dipilih karena teks putih di atasnya 8.43:1, jauh di
       atas 4.5:1, jadi gradasi tidak pernah menipiskan teks di ujung mana pun. */
    --cta-from: #b4471f;
    --cta-to: #8a2f0c;
    --cta-from-hover: #9c3b19;
    --cta-to-hover: #7d280a;

    --success: #1f7a4d;
    --success-soft: #eaf4ef;
    --danger: #b4231f;
    --danger-soft: #fbeeed;
    --warning: #8a5a00;
    --warning-soft: #fbf1e2;

    --radius-sm: 4px;
    --radius: 8px;
    --radius-lg: 12px;
    --radius-pill: 9999px;

    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-5: 24px;
    --space-6: 32px;
    --space-7: 48px;
    --space-8: 64px;

    --shadow-card: 0 1px 1px rgb(31 29 27 / 4%), 0 2px 4px rgb(31 29 27 / 4%);
    --shadow-pop: 0 8px 24px rgb(31 29 27 / 12%);

    --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    --transition: 200ms ease;

    --container: 1120px;
    --tap: 44px;

    /* Tinggi bar navigasi bawah. Isi konten memakai token yang sama supaya
       baris terakhir tabel tidak pernah tertutup bilah tetap. */
    --bottom-nav-h: 62px;

    /* Tinggi topbar area privat: target sentuh plus padding atas bawah plus
       garis bawah. Dipakai juga sebagai batas atas panel menu, jadi panel
       tidak pernah menutupi tombol yang membukanya. */
    --topbar-h: calc(var(--tap) + var(--space-2) * 2 + 1px);
}

/*
 * Warna sebagai penunjuk arah modul. Empat hue, masing-masing satu tugas:
 * rundown dan halaman umum clay (warna merek), kas dan tabungan teal,
 * talangan rose, galeri violet. Satu halaman hanya memakai satu hue, jadi
 * palet aktifnya tetap satu aksen di atas netral hangat.
 *
 * `--accent` tetap warna solid karena link, cincin fokus, dan teks butuh
 * warna yang bisa diuji kontras langsung. Gradien hanya mengisi permukaan
 * tombol, dan kedua ujungnya adalah dua titik pada satu ramp hue, bukan dua
 * warna berbeda. Ujung paling terang di mode terang (dan paling gelap di
 * mode gelap) tetap di atas ambang kontras, jadi teks tombol punya rasio
 * kontras yang sama di seluruh lebar gradien.
 */
[data-hue='teal'] {
    --accent: #0d6f66;
    --accent-hover: #0a5952;
    --accent-soft: #e6f4f2;
    --focus: #0d6f66;
    --cta-from: #0d6f66;
    --cta-to: #07463f;
    --cta-from-hover: #0a5952;
    --cta-to-hover: #063833;
}

[data-hue='rose'] {
    --accent: #a81f4f;
    --accent-hover: #8c1740;
    --accent-soft: #fceaf0;
    --focus: #a81f4f;
    --cta-from: #a81f4f;
    --cta-to: #701238;
    --cta-from-hover: #8c1740;
    --cta-to-hover: #600f30;
}

[data-hue='violet'] {
    --accent: #6b3bc4;
    --accent-hover: #59309f;
    --accent-soft: #f0eafd;
    --focus: #6b3bc4;
    --cta-from: #6b3bc4;
    --cta-to: #4a2686;
    --cta-from-hover: #59309f;
    --cta-to-hover: #3f1f75;
}

[data-theme='dark'] {
    color-scheme: dark;

    --canvas: #191817;
    --surface: #232120;
    --surface-2: #2c2a28;
    --surface-3: #35322f;
    --hairline: #37332f;
    --hairline-strong: #4a4540;
    --control-border: #7c746c;

    --ink: #f5f2ef;
    --ink-muted: #b8b2ab;
    --ink-subtle: #a49d95;
    --on-accent: #191817;

    --accent: #e08a5f;
    --accent-hover: #ee9c74;
    --accent-soft: #33241c;
    --focus: #e08a5f;

    /* Mode gelap membalik ramp: dari clay ke terakota yang lebih terang, dengan
       teks tinta. Ujung #f7b491 memberi 10.04:1 terhadap tinta, jadi gradasi
       terang di mode gelap tidak pernah edgy di ujung manapun. */
    --cta-from: #e08a5f;
    --cta-to: #f7b491;
    --cta-from-hover: #e08257;
    --cta-to-hover: #f0a882;

    --success: #7fd1a6;
    --success-soft: #1e2f27;
    --danger: #f08a85;
    --danger-soft: #33211f;
    --warning: #e3b341;
    --warning-soft: #322a19;

    --shadow-card: 0 1px 1px rgb(0 0 0 / 30%), 0 2px 4px rgb(0 0 0 / 24%);
    --shadow-pop: 0 8px 24px rgb(0 0 0 / 45%);
}

/* Mode gelap untuk tiga hue selain clay. Selector harus dua atribut
   sekaligus, `[data-theme='dark'][data-hue=...]` dengan specificity (0,2,0),
   karena blok `[data-hue=...]` di atas hanya (0,1,0) dan kalah dari blok tema
   yang dibuat belakangan. */
[data-theme='dark'][data-hue='teal'] {
    --accent: #45c2b4;
    --accent-hover: #6bd6c8;
    --accent-soft: #16302e;
    --focus: #45c2b4;
    --cta-from: #45c2b4;
    --cta-to: #86e0d4;
    --cta-from-hover: #3fb3a5;
    --cta-to-hover: #7ad9ce;
}

[data-theme='dark'][data-hue='rose'] {
    --accent: #f0779f;
    --accent-hover: #f79ab7;
    --accent-soft: #341c26;
    --focus: #f0779f;
    --cta-from: #f0779f;
    --cta-to: #fbb0c6;
    --cta-from-hover: #e86a92;
    --cta-to-hover: #f8adc2;
}

[data-theme='dark'][data-hue='violet'] {
    --accent: #b494f5;
    --accent-hover: #c7aef8;
    --accent-soft: #241a3d;
    --focus: #b494f5;
    --cta-from: #b494f5;
    --cta-to: #d5c2fb;
    --cta-from-hover: #a88bf0;
    --cta-to-hover: #ceb8f9;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

html {
    -webkit-text-size-adjust: 100%;
    /* Warna latar dipasang di html, bukan hanya di body. Saat toolbar browser
       bergeser ketika menggulir, area yang baru terbuka di luar dokumen
       dicat dengan warna root; tanpa ini browser mengisinya dengan hitam
       (tema gelap memakai color-scheme: dark, jadi kanvas bawaannya hitam). */
    background: var(--canvas);
    /* Dokumen minimal setinggi layar, jadi tidak ada sisa area di bawah konten
       yang tidak dicat saat halaman pendek atau layar berubah tinggi. */
    min-height: 100%;
    /* Tarikan lewat ujung halaman tidak memperlihatkan area di luar dokumen. */
    overscroll-behavior-y: none;
    /* Two quick taps no longer zoom the page. `manipulation` keeps scrolling
       and pinch zoom, so low-vision users can still enlarge the page. */
    touch-action: manipulation;
}

/* touch-action is not inherited, and Safari resolves the double-tap against
   the element that was tapped, so the controls say it themselves. */
a,
button,
input,
select,
textarea,
label,
summary {
    touch-action: manipulation;
}

body {
    margin: 0;
    background: var(--canvas);
    color: var(--ink);
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;

    /* Almost every word on screen was typed by a member: names, usernames,
       activity titles, notes. Indonesian compounds run long, and one without a
       break opportunity does not fit its box, it just leaves it.

       `anywhere` rather than `break-word`. Both break the same word at the same
       place, but only `anywhere` also lets a flex or grid child report a
       smaller minimum width. With `break-word` the minimum stays at the length
       of the longest word, so the box is still too wide and the break never
       gets its chance. Measured on a 48-character unbroken title: with
       `break-word` the dashboard laid out at 508px inside a 360px viewport.

       Inherited, so it reaches every card, table cell, badge and label without
       each of them having to remember. */
    overflow-wrap: anywhere;
}

h1,
h2,
h3,
h4 {
    margin: 0;
    font-weight: 600;
    line-height: 1.2;
    /* Tight tracking on headings is the one place the type gets compressed:
       long Indonesian words stop looking loose at large sizes. */
    letter-spacing: -0.02em;
    /* No wrap rule here on purpose. Body already sets `anywhere` and it is
       inherited, so this block used to be the one place that opted back out of
       it. `break-word` breaks the word but keeps the minimum width at the full
       word, which is enough to push a heading wider than the card around it:
       the rundown title laid its page out at 625px inside a 320px phone. */
}

/* Headings get a smaller step on a phone instead of the desktop size shrunk
   to fit: the mobile floor is where long words would otherwise touch the
   edges of the screen. */
h1 {
    font-size: clamp(24px, 4.4vw + 8px, 30px);
    letter-spacing: -0.03em;
}

h2 {
    font-size: clamp(19px, 3.2vw + 7px, 22px);
}

h3 {
    font-size: 17px;
}

/* Nama anggota di kartu perlu tetap sebesar heading lain meski memakai h2,
   supaya level dokumen dan besar huruf tidak saling mengikat. */
:root {
    /* Skala teks. --text-xs, --text-base dan --text-lg dipakai di CSS dan di
       view tetapi tidak pernah didefinisikan, jadi font-size-nya dianggap
       tidak valid dan teksnya jatuh ke ukuran induk (label ringkasan sebesar
       isi, judul kartu target sebesar paragraf). */
    --text-xs: 12px;
    --text-sm: 13px;
    --text-base: 15px;
    --text-lg: 17px;
    --text-card-title: 17px;
}

p {
    margin: 0 0 var(--space-4);
}

a {
    color: var(--accent);
    text-decoration: none;
    transition: color var(--transition);
}

a:hover {
    color: var(--accent-hover);
    text-decoration: underline;
    text-underline-offset: 3px;
}

:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

img {
    max-width: 100%;
    height: auto;
    display: block;
}

hr {
    border: 0;
    border-top: 1px solid var(--hairline);
    margin: var(--space-5) 0;
}

code {
    font-family: ui-monospace, 'SF Mono', Menlo, monospace;
    font-size: 0.9em;
    background: var(--surface-2);
    padding: 1px 5px;
    border-radius: var(--radius-sm);
}

/* ---------- layout ---------- */

.container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: var(--space-4);
}

.stack > * + * {
    margin-top: var(--space-4);
}

.stack-sm > * + * {
    margin-top: var(--space-2);
}

.stack-lg > * + * {
    margin-top: var(--space-6);
}

.row {
    display: flex;
    gap: var(--space-3);
    align-items: center;
    flex-wrap: wrap;
}

.row-between {
    display: flex;
    gap: var(--space-3);
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
}

/* Ringkasan label dan nilai untuk halaman detail. Dua kolom, dan tetap satu
   kolom di layar sempit supaya nilai panjang tidak terdorong keluar. */
.detail-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-2) var(--space-4);
    margin: 0;
}

@media (min-width: 480px) {
    .detail-list {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.detail-list > div {
    display: grid;
    gap: var(--space-1);
    min-width: 0;
}

.detail-list dt {
    font-size: var(--text-xs);
    color: var(--ink-muted);
}

.detail-list dd {
    margin: 0;
    overflow-wrap: anywhere;
}

/* Panel jatah per orang hanya relevan kalau mode `per_anggota` yang dipilih,
   dan tabelnya hanya relevan kalau "bagi rata" tidak dicentang. Dua-duanya
   deciding pakai `:has()` supaya tidak perlu JavaScript: kalau `:has()` tidak
   didukung, panelnya tetap terbuka dan form tetap bisa diisi, cuma keduanya
   tampil bersamaan. */
.target-mode > .field {
    margin-top: var(--space-4);
}

/* Aturan `display: none` ditaruh di dalam `@supports` supaya browser lama
   menampilkan panelnya: isinya lalu diabaikan controller saat mode bebas. */
@supports selector(:has(*)) {
    .target-mode:has(input[name="mode"][value="bebas"]:checked) > .field {
        display: none;
    }

    .target-mode:has(input[name="dibagi_rata"]:checked) .table-wrap {
        display: none;
    }
}

/* Target tabungan. Bar Progress penuh yang bisa dibaca tanpa warna dan tanpa
   JavaScript, jadi angkanya di dalam batang itu yang jadi utama. */
.progress {
    height: var(--space-3);
    background: var(--surface-2);
    border-radius: var(--radius-pill);
    overflow: hidden;
}

.progress-bar {
    height: 100%;
    background: var(--accent);
    border-radius: inherit;
    min-width: var(--space-3);
}

/* Persentase dicetak di dalam batang dengan ukuran kecil supaya tidak menambah
   tinggi kartu. Warnanya mengikuti isi batang supaya tidak pernah jadi teks
   putih di atas kuning muda. */
.progress-figure {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    min-height: 22px;
}

/* Trek mengisi seluruh baris. Sebelumnya .progress adalah item flex tanpa
   lebar, jadi lebarnya 0 dan hanya batang yang tampil, sebagai gumpalan
   berwarna tanpa trek abu-abu di belakangnya. */
.progress-figure .progress {
    position: absolute;
    inset: 0;
    height: auto;
}

.progress-figure-value {
    position: relative;
    padding-inline: var(--space-2);
    font-size: var(--text-xs);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

.grow {
    flex: 1 1 auto;
    min-width: 0;
}

/* Judul kartu dan lencananya satu baris: judul boleh menyusut dan turun baris,
   lencana tidak ikut terdorong ke bawah judul. */
.row-between:has(> .grow):has(> .badge) {
    flex-wrap: nowrap;
    align-items: flex-start;
}

.row-between > .badge {
    flex-shrink: 0;
}

/* The base grid gets an explicit single column for the same reason the
   modifiers above use `minmax(0, 1fr)` instead of `auto`. An implicit `auto`
   track takes its minimum from the content, and that minimum is not capped by
   the space available, so one long word widened the track and with it the
   whole card, the whole column and the page. The track now stops at the width
   it was given, and the word inside it is broken by `overflow-wrap` on body. */
.grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
}

@media (min-width: 768px) {
    .grid-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .grid-3 {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }

    .grid-4 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

@media (min-width: 1024px) {
    .grid-4 {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

.hide-mobile {
    display: none;
}

@media (min-width: 768px) {
    .hide-mobile {
        display: revert;
    }
}

.hide-desktop {
    display: revert;
}

@media (min-width: 768px) {
    .hide-desktop {
        display: none;
    }
}

/* ---------- typography helpers ---------- */

.text-sm {
    font-size: 13px;
}

.text-xs {
    font-size: 12px;
}

.text-muted {
    color: var(--ink-muted);
}

.text-subtle {
    color: var(--ink-subtle);
}

.text-accent {
    color: var(--accent);
}

.eyebrow {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-subtle);
}

.num {
    font-variant-numeric: tabular-nums;
    text-align: right;
    white-space: nowrap;
}

.num-left {
    font-variant-numeric: tabular-nums;
}

/* ---------- buttons ---------- */

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: 0 var(--space-4);
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    text-align: center;
    /* A label is a fixed word, not member text. `overflow-wrap: anywhere` on
       body would otherwise let a button shrink to one letter wide. */
    white-space: nowrap;
    overflow-wrap: normal;
    flex-shrink: 0;
    transition: background var(--transition), border-color var(--transition),
        color var(--transition);
}

.btn:hover {
    text-decoration: none;
}

/* Gradien hanya di permukaan tombol. Ini satu-satunya tempat di aplikasi ini
   yang memakai gradien, jadi "tombol utama" dan "warna merek" selalu dibaca
   sebagai satu hal. Ujungnya dua titik pada ramp hue yang sama, bukan dua
   warna, jadi gradasi ini tidak pernah menjadi rainbow(R-01). */
.btn-primary {
    background-color: var(--cta-from);
    background-image: linear-gradient(135deg, var(--cta-from), var(--cta-to));
    color: var(--on-accent);
}

.btn-primary:hover {
    background-image: linear-gradient(135deg, var(--cta-from-hover), var(--cta-to-hover));
    color: var(--on-accent);
}

.btn-secondary {
    background: var(--surface);
    border-color: var(--control-border);
    color: var(--ink);
}

.btn-secondary:hover {
    background: var(--surface-2);
    color: var(--ink);
}

.btn-ghost {
    background: transparent;
    color: var(--ink-muted);
}

.btn-ghost:hover {
    background: var(--surface-2);
    color: var(--ink);
}

.btn-danger {
    background: var(--surface);
    border-color: var(--danger);
    color: var(--danger);
}

.btn-danger:hover {
    background: var(--danger-soft);
    color: var(--danger);
}

/* Tombol padat tetap setinggi tombol biasa. Kepadatan visualnya datang dari
   font dan padding yang lebih rapat, bukan dari target sentuh yang mengecil;
   versi 36px menyulitkan pengguna sentuh, termasuk di laptop sentuh yang
   tetap melaporkan pointer:fine. */
.btn-sm {
    min-height: var(--tap);
    padding: 0 var(--space-3);
    font-size: 13px;
}

.btn-block {
    width: 100%;
}

.btn[disabled],
.btn[aria-disabled='true'] {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ---------- forms ---------- */

.field {
    display: block;
    /* A grid or flex child defaults to min-width:auto, so a date input's
       intrinsic width held the whole field, and the card, open. */
    min-width: 0;
}

.field + .field {
    margin-top: var(--space-4);
}

.label {
    display: block;
    margin-bottom: var(--space-1);
    font-size: 13px;
    font-weight: 500;
    color: var(--ink);
}

.hint {
    margin: var(--space-1) 0 0;
    font-size: 12px;
    color: var(--ink-subtle);
}

.input,
.select,
.textarea {
    width: 100%;
    min-height: var(--tap);
    padding: 10px var(--space-3);
    border: 1px solid var(--control-border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    /* 16px is the floor: iOS Safari zooms the whole page when a field below
       16px gets focus, and does not zoom back out afterwards. */
    font-size: 16px;
    min-width: 0;
    max-width: 100%;
    transition: border-color var(--transition), box-shadow var(--transition);
}

/* iOS Safari draws date, time and number inputs at an intrinsic width and
   ignores width:100%, so they stuck out of the card on phones. Dropping the
   native appearance lets the box obey the width above, and the inner value is
   left-aligned to match the text inputs next to it. */
.input[type='date'],
.input[type='time'],
.input[type='month'],
.input[type='datetime-local'],
.input[type='number'] {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    line-height: 1.4;
}

.input[type='date']::-webkit-date-and-time-value,
.input[type='time']::-webkit-date-and-time-value,
.input[type='month']::-webkit-date-and-time-value,
.input[type='datetime-local']::-webkit-date-and-time-value {
    min-width: 0;
    text-align: left;
}

/* File inputs carry a long file name and a native button; both must stay
   inside the box. */
.input[type='file'] {
    padding: 6px var(--space-3);
    font-size: 14px;
    line-height: 1.6;
}

.input[type='file']::file-selector-button {
    margin-right: var(--space-3);
    padding: 6px var(--space-3);
    border: 0;
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    color: var(--ink);
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
}

.textarea {
    min-height: 96px;
    resize: vertical;
}

.select {
    appearance: none;
    padding-right: var(--space-6);
    background-image: linear-gradient(45deg, transparent 50%, currentcolor 50%),
        linear-gradient(135deg, currentcolor 50%, transparent 50%);
    background-position: right 18px center, right 13px center;
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
}

.input:focus,
.select:focus,
.textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
}

.input[aria-invalid='true'],
.select[aria-invalid='true'],
.textarea[aria-invalid='true'] {
    border-color: var(--danger);
}

.checkbox-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
}

.checkbox-row input[type='checkbox'],
.checkbox-row input[type='radio'] {
    width: 18px;
    height: 18px;
    accent-color: var(--accent);
}

/* ---------- disclosure group ---------- */

/* The public content form outgrew one screen long ago. Each group folds away
   so the admin lands on the two fields people edit most, not on a wall of
   empty textareas. Native <details> keeps it keyboard-navigable with no
   script, and it still opens and closes without JavaScript. */
.disclosure {
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
}

.disclosure > summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--tap);
    padding: var(--space-3) var(--space-4);
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.disclosure > summary::-webkit-details-marker {
    display: none;
}

.disclosure > summary::after {
    content: '';
    flex: none;
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid var(--ink-subtle);
    border-bottom: 2px solid var(--ink-subtle);
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--transition);
}

.disclosure[open] > summary::after {
    transform: translateY(2px) rotate(-135deg);
}

.disclosure[open] > summary {
    border-bottom: 1px solid var(--hairline);
}

.disclosure-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-4);
}

.disclosure-note {
    margin: 0;
}

.form-error {
    margin: var(--space-1) 0 0;
    font-size: 12px;
    color: var(--danger);
}

.form-grid {
    display: grid;
    /* Explicit track for the same reason as .grid: an implicit `auto` track
       takes its minimum from its content, so one wide control widened every
       field in the form. */
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
}

@media (min-width: 768px) {
    .form-grid-2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .form-grid-3 {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

/* ---------- surfaces ---------- */

.card {
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-card);
}

.card-pad {
    padding: var(--space-5);
}

@media (min-width: 768px) {
    .card-pad {
        padding: var(--space-6);
    }
}

.card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}

/* Card tanpa .card-pad (isinya tabel yang harus selebar card) tidak punya
   padding, jadi judulnya menempel ke garis tepi. Beri jarak yang sama dengan
   .card-pad supaya judul, paragraf pembuka, dan isi tabel satu garis kiri. */
.card:not(.card-pad) > .card-head {
    padding: var(--space-5) var(--space-5) 0;
}

.card-head h2 {
    margin: 0;
    min-width: 0;
}

.panel {
    background: var(--surface-2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    padding: var(--space-4);
}

.divider {
    height: 1px;
    background: var(--hairline);
    margin: var(--space-4) 0;
}

/* ---------- stats ---------- */

.stat {
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    padding: var(--space-4);
}

.stat-label {
    font-size: 12px;
    color: var(--ink-subtle);
}

.stat-value {
    margin-top: var(--space-1);
    font-size: 22px;
    font-weight: 600;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

/* Angka rupiah pada kartu ringkasan. Angkanya cepat jadi panjang
   ("Rp 12.500.000"), jadi ukurannya turun di layar sempit supaya masih muat
   di dalam kolom yang setengah lebarnya. */
.stat-value-lg {
    font-size: clamp(19px, 3.4vw + 8px, 26px);
}

.stat-note {
    margin-top: 2px;
    font-size: 12px;
    color: var(--ink-muted);
}

/* ---------- badges ---------- */

.badge {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    font-size: 12px;
    font-weight: 500;
    line-height: 1.6;
    background: var(--surface-2);
    color: var(--ink-muted);
    /* Badges hold short system text (a status, an amount). Keep them whole
       instead of letting `anywhere` split "Nonaktif" or "Rp 1.250.000". */
    white-space: nowrap;
    overflow-wrap: normal;
    flex-shrink: 0;
}

.badge-dot {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: currentcolor;
    flex: none;
}

.badge-success {
    background: var(--success-soft);
    color: var(--success);
}

.badge-danger {
    background: var(--danger-soft);
    color: var(--danger);
}

.badge-warning {
    background: var(--warning-soft);
    color: var(--warning);
}

.badge-accent {
    background: var(--accent-soft);
    color: var(--accent);
}

/* ---------- alerts ---------- */

.alert {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    font-size: 14px;
}

.alert-success {
    border-color: var(--success);
    background: var(--success-soft);
    color: var(--success);
}

.alert-error {
    border-color: var(--danger);
    background: var(--danger-soft);
    color: var(--danger);
}

.alert-info {
    border-color: var(--control-border);
    background: var(--surface-2);
    color: var(--ink);
}

/* ---------- tables ---------- */

.table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.table {
    width: 100%;
    border-collapse: collapse;
    font-size: 14px;
}

.table th,
.table td {
    padding: var(--space-3) var(--space-3);
    text-align: left;
    border-bottom: 1px solid var(--hairline);
    vertical-align: middle;
}

.table th {
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-subtle);
    white-space: nowrap;
}

.table tbody tr:last-child td {
    border-bottom: 0;
}

.table tbody tr:hover {
    background: var(--surface-2);
}

/* Below the tablet breakpoint a wide money table stops being a table: each
   row becomes a card, so nothing needs sideways scrolling to read.

   The cell marked data-title carries the row's identity, so it is set as the
   card heading and drops its label. The action cell moves up beside it.
   Stacking every cell as a labelled row instead made one record taller than
   a phone screen: a five column ledger row needed six lines. */
@media (max-width: 767px) {
    .table-stack thead {
        display: none;
    }

    .table-stack tbody tr {
        display: grid;
        /* One column. The action buttons used to sit in a second `min-content`
           column beside the name, but `overflow-wrap: anywhere` on body makes
           the minimum width of any text a single letter, so that column
           collapsed and "Ubah" / "Nonaktifkan" were drawn one letter per line.
           The buttons now get their own full-width line under the record. */
        grid-template-columns: minmax(0, 1fr);
        row-gap: var(--space-2);
        align-items: baseline;
        /* Side padding: the card around a table has none, so without it the
           name and the badges sat flush against the card border. */
        padding: var(--space-3) var(--space-5);
        border-bottom: 1px solid var(--hairline);
    }

    /* The card's own border closes the last record. */
    .table-stack tbody tr:last-child {
        border-bottom: 0;
    }

    .table-stack tbody td {
        display: flex;
        grid-column: 1;
        gap: var(--space-3);
        justify-content: space-between;
        align-items: baseline;
        border: 0;
        padding: 0;
        text-align: right;
        min-width: 0;
    }

    .table-stack tbody td::before {
        content: attr(data-label);
        font-size: 12px;
        font-weight: 600;
        color: var(--ink-subtle);
        text-align: left;
        flex: none;
    }

    /* Heading cell: no label, because the text is the record's name rather
       than a value that needs one. */
    .table-stack tbody td[data-title] {
        display: block;
        text-align: left;
        font-weight: 500;
    }

    .table-stack tbody td[data-title]::before {
        content: none;
    }

    /* Action buttons wrap onto their own line below the record, and wrap among
       themselves when three do not fit side by side. Without the "Aksi" label
       the line costs one button height and nothing more. */
    .table-stack tbody td[data-label='Aksi'] {
        display: flex;
        flex-wrap: wrap;
        justify-content: flex-start;
        align-items: center;
        gap: var(--space-2);
        text-align: left;
        padding-top: var(--space-1);
    }

    .table-stack tbody td[data-label='Aksi']::before {
        content: none;
    }

    /* Rows hold a nested <form> and a loose button: lay them out as one
       wrapping line and drop the stray top margin the desktop view needed. */
    .table-stack tbody td[data-label='Aksi'] > * {
        margin-top: 0 !important;
    }

    /* A row that spans the whole table is a form, not a record. It must stay
       one block so its fields lay out side by side. */
    .table-stack tbody td[colspan] {
        display: block;
        grid-column: 1 / -1;
        text-align: left;
    }

    .table-stack tbody td[colspan]::before {
        content: none;
    }
}

/* ---------- site header ---------- */

.site-header {
    position: sticky;
    top: 0;
    z-index: 30;
    background: var(--canvas);
    border-bottom: 1px solid var(--hairline);
    transition: background var(--transition), border-color var(--transition);
}

.site-header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-height: 60px;
}

.brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--ink);
    font-weight: 600;
    letter-spacing: -0.02em;
}

.brand:hover {
    color: var(--ink);
    text-decoration: none;
}

.brand-mark {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--on-accent);
    font-size: 13px;
    font-weight: 700;
}

.site-nav {
    display: none;
    align-items: center;
    gap: var(--space-1);
}

/* On phones the nav becomes a panel under the header, toggled by the hamburger.
   It is a plain CSS layout change, so it works even before app.js loads. */
.site-nav.is-open {
    display: flex;
    position: absolute;
    inset: 100% 0 auto 0;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: var(--space-2);
    background: var(--surface);
    border-bottom: 1px solid var(--hairline);
    box-shadow: var(--shadow-pop);
}

@media (min-width: 768px) {
    .site-nav {
        display: flex;
    }
}

/* Hanya tautan nav biasa yang diberi gaya di sini. Tombol di dalam nav
   (Masuk / Dashboard) punya class .btn, dan tanpa :not(.btn) aturan nav ini
   akan menimpa warna teks tombolnya sendiri. */
.site-nav a:not(.btn) {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
    padding: 0 var(--space-3);
    border-radius: var(--radius);
    color: var(--ink-muted);
    font-size: 14px;
    font-weight: 500;
}

.site-nav a:not(.btn):hover {
    background: var(--surface-2);
    color: var(--ink);
    text-decoration: none;
}

.site-nav a:not(.btn)[aria-current='page'] {
    color: var(--ink);
    background: var(--surface-2);
}

.nav-toggle {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
}

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink-muted);
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.icon-btn:hover {
    background: var(--surface-2);
    color: var(--ink);
}

.icon-btn svg {
    width: 18px;
    height: 18px;
}

/* ---------- app shell ---------- */

.app-shell {
    display: grid;
    background: var(--canvas);
    min-height: 100vh;
    /* 100vh adalah tinggi layar ketika toolbar browser tersembunyi. Saat
       toolbar tampil, layar yang terlihat lebih pendek, jadi bagian bawah
       halaman terpotong di balik toolbar. dvh mengikuti tinggi yang terlihat. */
    min-height: 100dvh;
    grid-template-columns: 1fr;
}

.app-main {
    min-width: 0;
    padding: var(--space-4);
    /* Ruang untuk bilah navigasi bawah, ditambah jarak lega agar baris
       terakhir tidak menempel ke sana. */
    padding-bottom: calc(var(--bottom-nav-h) + var(--space-6));
}

/* Sidebar hanya hidup di lebar laptop. Di bawah itu navigasi bawah yang
   dipakai, jadi sidebar disembunyikan supaya tidak ada dua daftar menu
   bertumpuk di layar yang sempit. Tombol menu di topbar membukanya lagi
   sebagai panel, karena grup Admin tidak ada di navigasi bawah dan tanpa
   ini halaman pengaturan publik tidak terjangkau di ponsel. */
.app-sidebar {
    display: none;
}

@media (max-width: 1023px) {
    /* Panel menempel di bawah topbar, bukan di dalam aliran. Kalau ikut
       mengalir, menggulir halaman akan menyeretnya ke atas dan tombol menu
       kelihatan tidak bekerja. Batas tingginya memakai token topbar supaya
       panel tidak pernah menutupi tombol yang membukanya, dan daftar menu
       yang lebih panjang dari layar digulir di dalam panel. */
    .app-sidebar.is-open {
        position: fixed;
        top: var(--topbar-h);
        right: 0;
        left: 0;
        z-index: 19;
        display: block;
        /* Navigasi bawah (z-index 40) menutupi dasar layar, jadi tinggi panel
           menyisakan tempat untuknya. Tanpa ini baris menu terakhir, yang
           biasanya grup Admin, tertutup dan tidak bisa disentuh. */
        max-height: calc(100dvh - var(--topbar-h) - var(--bottom-nav-h) - env(safe-area-inset-bottom));
        overflow-y: auto;
        overscroll-behavior: contain;
        padding: var(--space-4);
        border-radius: 0 0 var(--radius-lg) var(--radius-lg);
        border-bottom: 1px solid var(--hairline);
        background: var(--surface);
        box-shadow: var(--shadow-pop);
    }

    /* Di bawah lebar laptop, topbar sudah membawa nama kelompok dan menu
       akun, jadi brand dan baris akun di sidebar cuma mengulang isi yang
       sama. */
    .app-sidebar-brand,
    .app-sidebar-foot,
    .app-sidebar .divider {
        display: none;
    }

    /* Latar redup di bawah topbar saat panel terbuka. Dipasang di body oleh
       app.js (kelas has-nav-open), karena panel yang fixed tidak bisa
       menggambar latar di luar kotaknya sendiri tanpa menimpa isinya. Ketukan
       di latar menutup panel lewat penangan klik di luar yang sudah ada. */
    body.has-nav-open {
        overflow: hidden;
    }

    body.has-nav-open::before {
        content: '';
        position: fixed;
        inset: var(--topbar-h) 0 0 0;
        z-index: 18;
        background: rgb(0 0 0 / 40%);
    }
}

@media (min-width: 1024px) {
    .app-shell {
        grid-template-columns: 248px 1fr;
    }

    .app-sidebar {
        display: block;
        border-right: 1px solid var(--hairline);
        padding: var(--space-5) var(--space-4);
        position: sticky;
        top: 0;
        height: 100vh;
        height: 100dvh;
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .app-main {
        padding: var(--space-6);
        padding-bottom: var(--space-7);
    }
}

.app-nav-group + .app-nav-group {
    margin-top: var(--space-5);
}

.app-nav-title {
    margin-bottom: var(--space-2);
    padding-inline: var(--space-3);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-subtle);
}

.app-nav a {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-height: var(--tap);
    padding: 0 var(--space-3);
    border-radius: var(--radius);
    color: var(--ink-muted);
    font-size: 14px;
    font-weight: 500;
}

.app-nav a:hover {
    background: var(--surface-2);
    color: var(--ink);
    text-decoration: none;
}

.app-nav a[aria-current='page'] {
    background: var(--accent-soft);
    color: var(--accent);
}

.app-nav a svg {
    width: 17px;
    height: 17px;
    flex: none;
}

.nav-count {
    margin-left: auto;
    padding: 1px 6px;
    border-radius: var(--radius-pill);
    background: var(--danger-soft);
    color: var(--danger);
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* Bottom navigation keeps the four most used modules one thumb-tap away on
   phones, where a sidebar would be a dead element. */
.bottom-nav {
    position: fixed;
    inset: auto 0 0 0;
    z-index: 40;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    background: var(--surface);
    border-top: 1px solid var(--hairline);
    padding-bottom: env(safe-area-inset-bottom);
}

.bottom-nav a {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-1);
    color: var(--ink-subtle);
    font-size: 12px;
    font-weight: 500;
    /* Umpan balik saat ditekan, karena layar sentuh tidak punya hover. */
    transition: color var(--transition), background var(--transition);
}

.bottom-nav a:active {
    background: var(--surface-2);
    color: var(--ink);
}

.bottom-nav a:hover {
    text-decoration: none;
}

.bottom-nav a[aria-current='page'] {
    color: var(--accent);
}

.bottom-nav svg {
    width: 20px;
    height: 20px;
}

@media (min-width: 1024px) {
    .bottom-nav {
        display: none;
    }
}

/* ---------- top bar (small screens only) ---------- */

/* The sidebar holds the theme switch and the logout button, and it is hidden
   below the laptop breakpoint. Without this bar a signed-in visitor on a phone
   has no way to leave the private area at all. */
.app-topbar {
    position: sticky;
    top: 0;
    z-index: 20;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    /* Padding dihitung di dalam tinggi, bukan menambahkannya, supaya tinggi
       topbar sama persis dengan token yang dipakai panel menu. */
    height: var(--topbar-h);
    padding: var(--space-2) var(--space-4);
    background: var(--surface);
    border-bottom: 1px solid var(--hairline);
}

/* Nama kelompok dan tombol menu satu grup, supaya justify-content: space-between
   pada topbar tetap menaruh avatar di kanan dan nama tetap bisa terpotong
   ellipsis kalau nama kelompoknya panjang. */
.app-topbar-lead {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
}

.app-topbar-name {
    min-width: 0;
    overflow: hidden;
    font-size: 14px;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* Tombol menu tanpa bingkai: ikonnya 20px di tengah kotak 44px, jadi geser
   setengah selisihnya (12px) ke kiri supaya ikon sejajar dengan tepi isi
   halaman. Dulu bingkainya ikut digeser sejauh padding topbar penuh, sehingga
   menempel ke tepi layar dan sudut kirinya terpotong. */
.app-topbar .nav-toggle {
    margin-left: calc(var(--space-3) * -1);
    border-color: transparent;
    background: transparent;
}

.app-topbar .nav-toggle:hover,
.app-topbar .nav-toggle[aria-expanded='true'] {
    background: var(--surface-2);
}

.app-topbar .nav-toggle:focus-visible {
    border-color: var(--accent);
}

.nav-toggle [hidden] {
    display: none;
}

.app-topbar .icon-btn {
    flex: none;
}

@media (min-width: 1024px) {
    .app-topbar {
        display: none;
    }
}

/* Account sheet: a compact list of who is signed in and the two actions that
   end or change the session. */
.account-sheet {
    width: min(320px, calc(100vw - 32px));
    padding: 0;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    color: var(--ink);
}

.account-sheet::backdrop {
    background: rgb(0 0 0 / 40%);
}

.account-sheet-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    border-bottom: 1px solid var(--hairline);
}

.account-sheet-head .avatar {
    flex: none;
}

.account-sheet-id {
    min-width: 0;
}

.account-sheet-id strong {
    display: block;
    font-size: 14px;
}

.account-sheet-id span {
    display: block;
    font-size: 12px;
    color: var(--ink-subtle);
}

.account-sheet-list {
    display: flex;
    flex-direction: column;
    padding: var(--space-2);
}

.account-sheet-list > * + * {
    margin-top: 2px;
}

.account-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    width: 100%;
    min-height: var(--tap);
    padding: var(--space-2) var(--space-3);
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--ink);
    font: inherit;
    font-size: 14px;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

.account-item:hover {
    background: var(--surface-2);
    text-decoration: none;
}

.account-item svg {
    flex: none;
    color: var(--ink-subtle);
}

.account-item-danger {
    color: var(--danger);
}

.account-item-danger svg {
    color: var(--danger);
}

.account-sheet form {
    display: contents;
}

/* ---------- avatar ---------- */

.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    overflow: hidden;
    background: var(--surface-2);
    color: var(--ink-muted);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.02em;
}

/* Member card on the public list: the photo sits beside the name so a long
   name wraps inside its own column instead of pushing the badge off-screen. */
.member-card {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
}

.member-card .avatar {
    flex: none;
}

.avatar-lg {
    width: 72px;
    height: 72px;
    font-size: 24px;
}

.avatar-btn {
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
}

.avatar-btn:hover {
    background: none;
}

/* ---------- page furniture ---------- */

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-5);
}

.page-title {
    font-size: clamp(21px, 3.6vw + 7px, 26px);
}

.section-gap {
    margin-top: var(--space-6);
}

.empty {
    padding: var(--space-5) var(--space-4);
    text-align: center;
    border: 1px dashed var(--hairline-strong);
    border-radius: var(--radius-lg);
    color: var(--ink-muted);
}

@media (min-width: 768px) {
    .empty {
        padding: var(--space-7) var(--space-4);
    }
}

.empty-title {
    margin-bottom: var(--space-2);
    font-weight: 600;
    color: var(--ink);
}

.tabs {
    display: flex;
    gap: var(--space-1);
    margin-bottom: var(--space-5);
    padding-bottom: var(--space-1);
    border-bottom: 1px solid var(--hairline);
    overflow-x: auto;
    scrollbar-width: thin;
}

.tabs a {
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0 var(--space-3);
    border-bottom: 2px solid transparent;
    color: var(--ink-muted);
    font-size: 14px;
    font-weight: 500;
    white-space: nowrap;
}

.tabs a:hover {
    color: var(--ink);
    text-decoration: none;
}

.tabs a[aria-current='page'] {
    border-bottom-color: var(--accent);
    color: var(--ink);
}

.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-5);
    font-size: 13px;
    color: var(--ink-muted);
}

.pagination-links {
    display: flex;
    gap: var(--space-2);
}

/* ---------- timeline ---------- */

.timeline {
    margin: 0;
    padding: 0;
    list-style: none;
}

.timeline li {
    position: relative;
    padding: 0 0 var(--space-5) var(--space-5);
    border-left: 1px solid var(--hairline);
}

.timeline li:last-child {
    border-left-color: transparent;
    padding-bottom: 0;
}

.timeline li::before {
    content: '';
    position: absolute;
    left: -5px;
    top: 6px;
    width: 9px;
    height: 9px;
    border-radius: var(--radius-pill);
    background: var(--accent);
}

.timeline-time {
    font-size: 12px;
    color: var(--ink-subtle);
    font-variant-numeric: tabular-nums;
}

.timeline-title {
    font-weight: 600;
}

/* ---------- rundown sheet ---------- */

.rundown-row {
    display: grid;
    grid-template-columns: 92px 1fr;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--hairline);
}

.rundown-row:last-child {
    border-bottom: 0;
}

.rundown-time {
    font-size: 13px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
}

.rundown-item {
    display: flex;
    gap: var(--space-2);
    align-items: center;
    width: 100%;
    min-height: 40px;
    padding: 0 var(--space-2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.rundown-item:hover {
    border-color: var(--accent);
    background: var(--accent-soft);
}

.rundown-meta {
    margin-top: 2px;
    font-size: 12px;
    color: var(--ink-subtle);
}

/* Pemisah hari di rundown yang membentang lebih dari satu tanggal. Baris
   pertama tiap tanggal baru diberi label supaya waktu 09:00 di hari kedua
   tidak terbaca sebagai sambungan langsung dari baris sebelumnya. */
.rundown-hari {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-4);
    padding: 0 var(--space-2);
    font-size: 12px;
    font-weight: 600;
    color: var(--ink-subtle);
}

.rundown-hari::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--hairline);
}

/* Baris editor rundown. Satu baris ditumpuk ke bawah di semua lebar.
   Versi lama meletakkan lima kolom sebaris di atas 1024px, tapi pengukuran
   di lebar 1024px hanya menyisakan 646px untuk isi baris: kolom waktu
   terpakai 48px dan kolom kegiatan 92px, sementara kolom penanggung melar
   sampai 650px. Satu baris mendatar butuh sekitar 1000px. */
.rundown-builder-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-3);
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--hairline);
}

.rundown-builder-row > * {
    min-width: 0;
}

.rundown-builder-row .input,
.rundown-builder-row .select {
    margin: 0;
}

/* Baris unggah galeri. Kolom berkas dan keterangan tetap menumpuk di layar
   sempit karena nama berkas yang panjang tidak boleh memaksa zoom. */
/* Jumlah kolom mengikuti lebar baris yang benar-benar tersedia, bukan lebar
   layar. Kolom berkas dan keterangan berbagi baris hanya bila masing-masing
   masih cukup lebar untuk input[type=file], yang punya lebar bawaan browser
   sekitar 216px dan tidak bisa dikecilkan. Media query yang hanya melihat
   lebar layar gagal di sini, karena form ini berada di dalam grid-2 yang ikut
   menyempit. 13.75rem = 220px, sedikit di atas lebar bawaan itu. */
.galeri-upload-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(13.75rem, 1fr));
    gap: var(--space-3);
    padding: var(--space-4) 0;
    border-bottom: 1px solid var(--hairline);
    align-items: start;
}

.galeri-upload-row > * {
    min-width: 0;
}

.galeri-upload-row .input,
.galeri-upload-row .textarea {
    margin: 0;
}

/* Tombol hapus baris berdiri sendiri. Aksi yang menghapus baris tidak
   sebaiknya berdampingan dengan kolom yang sedang diisi. */
.galeri-upload-row > [data-row-remove] {
    grid-column: 1 / -1;
    justify-self: start;
}

/* ---------- dialog ---------- */

.dialog {
    width: min(520px, calc(100vw - 32px));
    max-height: calc(100dvh - 32px);
    /* Formulir ubah anggota lebih tinggi dari layar ponsel, jadi dialognya
       sendiri yang menggulir, bukan halaman di belakangnya. */
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-5);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    color: var(--ink);
    box-shadow: var(--shadow-card);
}

.dialog::backdrop {
    background: rgba(0, 0, 0, 0.45);
}

.dialog h2 {
    margin: 0;
}

@media (min-width: 768px) {
    .dialog {
        padding: var(--space-6);
    }
}

/* ---------- target sentuh ----------
 *
 * Dua tingkat, sesuai perannya:
 *   - Kontrol (tombol, logo, baris daftar, tautan nav) 44px, karena inilah
 *     yang ditekung jari dan harusnya tidak perlu mengejar target.
 *   - Tautan teks yang berdiri sendiri 24px, sesuai WCAG 2.2 SC 2.5.8. Tautan
 *     di dalam paragraf sengaja dibiarkan kecil; memberi padding 44px padanya
 *     akan merusak jarak antarbaris.
 */
.brand {
    min-height: var(--tap);
}

/* Baris daftar di dasbor adalah tautan penuh, bukan teks di dalam paragraf. */
a.row-between {
    min-height: var(--tap);
}

/* Tautan yang sendirian jadi isi sebuah blok. */
.stat-note a,
.page-head a.text-sm,
.card-head a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
}

/* Tautan navigasi, termasuk yang di footer: ini tujuan yang dicari pengguna,
   bukan teks yang dibaca sambil menggulir. */
footer nav a {
    display: inline-flex;
    align-items: center;
    min-height: var(--tap);
}

/* Kembali ke daftar di atas halaman detail. Berdiri sendiri sebagai
   satu-satunya tautan di barisnya, jadi butuh area sentuh, bukan sekadar
   tinggi huruf. */
.back-link {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
}

/* Tautan di dalam sel tabel bukan kontrol, tapi tetap harus punya area
   sentuh. Padding vertikal cukup dan tidak mengubah cara teks membungkus
   seperti yang terjadi kalau display-nya diganti. */
.table a:not(.btn) {
    padding-block: 5px;
}

/* ---------- skeleton ---------- */

.skeleton {
    border-radius: var(--radius);
    background: linear-gradient(90deg, var(--surface-2) 25%, var(--surface-3) 37%, var(--surface-2) 63%);
    background-size: 400% 100%;
    animation: skeleton 1.4s ease infinite;
}

@keyframes skeleton {
    from {
        background-position: 100% 0;
    }

    to {
        background-position: 0 0;
    }
}

.spinner {
    width: 16px;
    height: 16px;
    border: 2px solid var(--hairline-strong);
    border-top-color: var(--accent);
    border-radius: var(--radius-pill);
    animation: spin 700ms linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

[hidden] {
    display: none !important;
}

/* ---------- gerak ----------
 *
 * MOTION 2: transisi dan satu reveal saat elemen masuk layar. Tidak ada
 * parallax, tidak ada halaman yang dipin, tidak ada loop tanpa akhir.
 *
 * State tersembunyi hanya berlaku kalau `html` sudah diberi kelas `js-motion`.
 * Tanpa JavaScript atau kalau pemuatannya gagal, tidak ada elemen yang
 * pernah tersembunyi, jadi isi halaman tidak bergantung pada skrip.
 *
 * Semua yang dianimasikan hanya `opacity` dan `transform`. `width`, `height`,
 * dan `box-shadow` sengaja tidak, karena ketiganya memaksa peramban menghitung
 * ulang tata letak atau lukisan pada tiap frame, dan di ponsel mahal itu
 * berarti gambarMgrapat turun.
 */
.js-motion [data-reveal] {
    opacity: 0;
    transform: translate3d(0, 10px, 0);
}

.js-motion [data-reveal].is-revealed {
    opacity: 1;
    transform: translate3d(0, 0, 0);
    transition: opacity 340ms ease, transform 340ms ease;
    transition-delay: var(--reveal-delay, 0ms);
}

/* Angka rupiah dihitung naik sekali saat masuk layar. `tabular-nums` diwarisi
   dari kelas .num supaya lebar kolom tidak ikut berdenyut tiap frame. */
.js-motion [data-count-to] {
    will-change: auto;
}

/* Bar progres diisi dari nol dengan mengubah skala, bukan lebar. Nilai akhir
   datang dari `--progress` yang dicetak view, jadi tanpa JS bar tetap menunjukkan
   angka yang benar dan yang dianimasikan hanya catanya. */
.progress-bar {
    transform: scaleX(var(--progress, 1));
    transform-origin: left center;
}

.js-motion .progress-bar {
    transform: scaleX(0);
    transform-origin: left center;
}

.js-motion .progress.is-filled .progress-bar {
    transform: scaleX(var(--progress, 1));
    transition: transform 620ms cubic-bezier(0.22, 0.7, 0.3, 1);
}

/* Kartu yang bisa diklik lifts sedikit saat kursor dessus. Ini satu-satunya
   tempat bayangan berubah, jadi elevasi tetap berarti "ini bisa ditekan". */
@media (hover: hover) {
    .kartu-tautan:hover {
        transform: translate3d(0, -2px, 0);
        box-shadow: var(--shadow-pop);
        transition: transform 180ms ease, box-shadow 180ms ease;
    }
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    /* `reduce` berarti tanpa gerak, bukan "gerak sangat cepat". State awal
       reveal dan bar progres dikembalikan ke bentuk finalnya. */
    .js-motion [data-reveal] {
        opacity: 1;
        transform: none;
    }

    .js-motion .progress-bar {
        transform: scaleX(var(--progress, 1));
    }
}

/* Cetak dan simpan PDF tidak pernah menjalankan reveal, dan isinya harus
   lengkap di halaman pertama. */
@media print {
    .js-motion [data-reveal] {
        opacity: 1;
        transform: none;
    }

    .js-motion .progress-bar {
        transform: scaleX(var(--progress, 1));
    }
}

/* ---------- print (rundown sheet) ---------- */

@media print {
    .site-header,
    .app-sidebar,
    .bottom-nav,
    .no-print {
        display: none !important;
    }

    body {
        background: #fff;
        color: #000;
    }

    .app-shell {
        display: block;
    }

    .card {
        border: 0;
        box-shadow: none;
    }

    @page {
        margin: 16mm;
    }
}

/* Album galeri. Kartu publik menampilkan satu sampul per kegiatan; halaman
   admin dan halaman detail menampilkan semua fotonya. Kolom mengikuti lebar
   yang tersedia, bukan lebar layar, karena daftar admin berada di dalam
   .grid-2 yang ikut menyempit. */
.galeri-kartu {
    display: block;
    min-width: 0;
    color: inherit;
}

.galeri-kartu-sampul {
    position: relative;
}

.galeri-kartu-sampul img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--radius-lg);
    border: 1px solid var(--hairline);
}

.galeri-kartu-jumlah {
    position: absolute;
    right: var(--space-2);
    bottom: var(--space-2);
    background: rgb(0 0 0 / 65%);
    color: #fff;
}

.galeri-album-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 130px), 1fr));
    gap: var(--space-3);
}

.galeri-album-item {
    display: grid;
    gap: var(--space-2);
    align-content: start;
    margin: 0;
    min-width: 0;
}

.galeri-album-item img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    border-radius: var(--radius);
    border: 1px solid var(--hairline);
}

.galeri-album-item figcaption {
    overflow-wrap: anywhere;
}

.galeri-detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
    gap: var(--space-5);
}

.galeri-detail-item {
    margin: 0;
    min-width: 0;
}

.galeri-detail-item img {
    width: 100%;
    border-radius: var(--radius-lg);
    border: 1px solid var(--hairline);
}

.galeri-detail-item figcaption {
    margin-top: var(--space-2);
    overflow-wrap: anywhere;
}
