@import "tailwind.css";

.material-symbols-outlined {
    font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24
}

body {
    font-family: 'Plus Jakarta Sans', sans-serif;
}

input[type=text], input[type=email], input[type=password], textarea, select {
    padding: 0.75em;
    border: 1px solid var(--color-border-default);
    border-radius: 0.375rem;
    width: 100%;
    box-sizing: border-box;
    transition: border-color 200ms, outline-color 200ms;
    color: var(--color-label-primary);
}

textarea {
    padding: 0.6em 0.75em 0.75em;
}

    input:hover, textarea:hover, select:hover {
        border-color: var(--color-border-strong);
    }

    input:focus, textarea:focus, select:focus {
        outline: 2px solid var(--color-primary);
        outline-offset: 2px;
        border-color: var(--color-primary);
    }

    input::placeholder, textarea::placeholder {
        color: var(--color-label-secondary);
    }

div.badge[data-badge]::after {
    content: attr(data-badge);
    position: absolute;
    font-size: 0.8em;
    width: 1.5em;
    height: 1.5em;
    background-color: var(--color-primary-500);
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    border-radius: 50%;
    top: 0;
    right: 0;
    margin: -0.5em;
}

.validation-message {
    color: var(--color-error);
    font-size: 12px;
    line-height: 16px;
    letter-spacing: 0.02em;
}

textarea.editable-text {
    resize: none;
    overflow: hidden;
}

input.editable-text, textarea.editable-text {
    padding: calc(var(--spacing)*2);
    border: 0;
    border-radius: 0.75rem;
}

    input.editable-text:hover, textarea.editable-text:hover {
        background-color: var(--color-surface-alt);
    }

    input.editable-text:focus, textarea.editable-text:focus {
        background-color: var(--color-surface-alt);
    }

nav a.active {
    color: var(--color-primary);
    background-color: var(--color-primary-subtle);
}

.btn-press {
    position: relative;
    overflow: hidden;
}

    .btn-press::after {
        content: "";
        position: absolute;
        inset: 0;
        background: rgba(255,255,255,.15);
        opacity: 0;
        transition: opacity 120ms;
    }

    .btn-press:active::after {
        opacity: 1;
    }

.z-70 {
    z-index: 70;
}

.modal-init {
    opacity: 0;
    transition: opacity ease-out 200ms;
}

.modal-open {
    opacity: 1;
}

::view-transition-old(root),
::view-transition-new(root) {
    animation: none;
}

/* Slide-out panel: keep as a separate transition layer (MAN-1, QA-3-1).
   - Old snapshot (navigating away from PlanStrand): slide out to the left.
   - New snapshot (navigating to PlanStrand): slide in from the left after a short
     delay so the handle enters once the main content has settled. */
::view-transition-old(slide-out-panel) {
    animation: panel-handle-slide-out 0.2s cubic-bezier(0.4, 0, 1, 1) both;
}

::view-transition-new(slide-out-panel) {
    animation: panel-handle-slide-in 0.25s cubic-bezier(0, 0, 0.2, 1) both;
    animation-delay: 350ms;
}

::view-transition-group(slide-out-panel) {
    z-index: 50;
}

@keyframes panel-handle-slide-in {
    from {
        opacity: 0;
        transform: translateX(-100%);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes panel-handle-slide-out {
    from {
        opacity: 1;
        transform: translateX(0);
    }

    to {
        opacity: 0;
        transform: translateX(-100%);
    }
}

/* Toast container: keep above transitioning content */
#toast-container {
    view-transition-name: toast-layer;
}

::view-transition-old(toast-layer),
::view-transition-new(toast-layer) {
    animation: none;
}

::view-transition-group(toast-layer) {
    z-index: 200;
}

::view-transition-old(main-content) {
    animation: md-slide-out 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;
}

::view-transition-new(main-content) {
    animation: md-slide-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;
    animation-delay: 200ms;
}

@keyframes md-slide-out {
    from {
        opacity: 1;
        transform: translateX(0);
    }

    to {
        opacity: 0;
        transform: translateX(30px);
    }
}

@keyframes md-slide-in {
    from {
        opacity: 0;
        transform: translateX(30px);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

@keyframes card-fade-in {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.card-fade-in {
    animation: card-fade-in 300ms cubic-bezier(0.25, 1, 0.5, 1) both;
}

@keyframes highlight-pulse {
    0% {
        background-color: var(--color-primary-subtle);
    }
    100% {
        background-color: transparent;
    }
}

/* Applied transiently to a textarea when a library item is added, then removed after 1.2s */
.animate-highlight {
    animation: highlight-pulse 1s ease-out both;
}

/* --- Generic plan-list item enter/exit --- */

/* Grid-rows collapse pattern: lets a list item grow from / collapse to zero
   height without measuring it. The wrapper sets `grid-template-rows: 1fr` and
   the inner element absorbs the row; animating to/from `0fr` animates height.

   Spacing between items is the wrapper's own margin-top (not a flex `gap` on
   the list), so it grows and shrinks with the item instead of snapping when
   the element is inserted or removed. Margin always transitions, so when the
   first item leaves (or a new first item arrives) the neighbour that gains or
   loses the "first" spacing eases into place too.

   Timings: enter grows height first and fades content in slightly behind it;
   exit fades content out first, then collapses the space it occupied. Keep
   the exit total (delay + collapse) in sync with ItemAnimation.ExitFallbackMs,
   which is only a fallback: callers wait on the browser's
   own animations via planItemAnim.waitForExit. */
.plan-item-anim {
    --plan-item-ease: cubic-bezier(0.22, 1, 0.36, 1);
    display: grid;
    grid-template-rows: 1fr;
    transition: margin-top 300ms var(--plan-item-ease);
}

/* The list container can widen the spacing with `--plan-item-gap`. */
.plan-item-anim + .plan-item-anim {
    margin-top: var(--plan-item-gap, 0.75rem);
}

.plan-item-anim > .plan-item-anim-inner {
    min-height: 0;
    /* Nested lists fall back to the default gap rather than inheriting. */
    --plan-item-gap: initial;
}

/* Only clip while animating so focus rings/outlines on inner inputs aren't
   cropped in the resting state. */
.plan-item-anim.is-entering > .plan-item-anim-inner,
.plan-item-anim.is-exiting > .plan-item-anim-inner {
    overflow: hidden;
}

/* Enter: rendered with `is-entering` already present, so @starting-style
   supplies the collapsed first frame without a second render round trip. */
.plan-item-anim.is-entering {
    transition: grid-template-rows 320ms var(--plan-item-ease),
                margin-top 320ms var(--plan-item-ease),
                opacity 260ms ease-out 80ms;
}

.plan-item-anim.is-entering > .plan-item-anim-inner {
    transition: transform 400ms var(--plan-item-ease);
}

@starting-style {
    .plan-item-anim.is-entering {
        grid-template-rows: 0fr;
        opacity: 0;
    }

    .plan-item-anim + .plan-item-anim.is-entering {
        margin-top: 0;
    }

    .plan-item-anim.is-entering > .plan-item-anim-inner {
        transform: translateY(-6px);
    }
}

/* Exit: fade (150ms), then collapse height and spacing (250ms). */
.plan-item-anim.is-exiting {
    grid-template-rows: 0fr;
    opacity: 0;
    pointer-events: none;
    transition: opacity 150ms ease-out,
                grid-template-rows 250ms cubic-bezier(0.4, 0, 0.2, 1) 120ms,
                margin-top 250ms cubic-bezier(0.4, 0, 0.2, 1) 120ms;
}

.plan-item-anim + .plan-item-anim.is-exiting {
    margin-top: 0;
}

/* When the first item leaves, its successor is about to become first and lose
   its spacing — ease that out alongside the collapse instead of snapping. */
.plan-item-anim.is-exiting:first-child + .plan-item-anim:not(.is-exiting) {
    margin-top: 0;
    transition: margin-top 250ms cubic-bezier(0.4, 0, 0.2, 1) 120ms;
}

/* Empty-state fade — applied when an empty placeholder is reintroduced after
   the last item has been removed. */
@keyframes empty-state-enter {
    from {
        opacity: 0;
        transform: scale(0.98);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

.animate-empty-enter {
    animation: empty-state-enter 300ms ease-out both;
}

/* --- Skeleton shimmer loading state --- */

@keyframes shimmer {
    0% {
        background-position: -200% 0;
    }
    100% {
        background-position: 200% 0;
    }
}

.skeleton {
    background: linear-gradient(
        90deg,
        var(--color-surface-alt) 25%,
        var(--color-surface-raised) 50%,
        var(--color-surface-alt) 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s ease-in-out infinite;
    border-radius: 0.375rem;
}

/* --- Celebration animation on plan finalise --- */

@keyframes celebrate-scale {
    0% {
        opacity: 0;
        transform: scale(0.5);
    }
    40% {
        opacity: 1;
        transform: scale(1.1);
    }
    60% {
        transform: scale(0.95);
    }
    80% {
        opacity: 1;
        transform: scale(1);
    }
    100% {
        opacity: 0;
        transform: scale(1);
    }
}

@keyframes celebrate-glow {
    0%, 100% {
        opacity: 0;
    }
    30%, 70% {
        opacity: 1;
    }
}

/* Celebration overlay — only plays when motion is allowed */
@media (prefers-reduced-motion: no-preference) {
    .celebration-icon {
        animation: celebrate-scale 1.5s cubic-bezier(0.22, 1, 0.36, 1) both;
    }

    .celebration-glow {
        animation: celebrate-glow 1.5s ease-in-out both;
    }
}

/* Dashboard entrance: staggered fade-up for each section */
@keyframes dashboard-enter {
    from {
        opacity: 0;
        transform: translateY(12px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.dashboard-entrance {
    /* `backwards` (not `both`) so the entrance's translateY doesn't linger as a finished
       transform — a lingering transform turns this into a containing block for any
       position:fixed descendant. The end keyframe equals the base style, so there is no
       visual difference. */
    animation: dashboard-enter 450ms cubic-bezier(0.25, 1, 0.5, 1) backwards;
}

/* Hides zones until the greeting entrance completes */
.dashboard-entrance-pending {
    opacity: 0;
}

/* Dashboard loading spinner: calm breathing pulse */
@keyframes dashboard-spinner-breathe {
    0%, 100% {
        opacity: 0.4;
        transform: scale(0.85);
    }
    50% {
        opacity: 1;
        transform: scale(1.05);
    }
}

.dashboard-spinner {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background-color: var(--color-primary);
    animation: dashboard-spinner-breathe 2s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}

.dashboard-spinner-fading {
    opacity: 0;
    transition: opacity 250ms ease;
}

/* Greeting word-by-word entrance */
@keyframes greeting-word-enter {
    from {
        opacity: 0;
        transform: translateY(8px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

@keyframes dashboard-fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

.greeting-word {
    display: inline-block;
    margin-right: 0.3em;
    opacity: 0;
    animation: greeting-word-enter 400ms ease both;
}

.greeting-subtitle-enter {
    opacity: 0;
    animation: dashboard-fade-in 350ms ease both;
}

.greeting-term-enter {
    opacity: 0;
    animation: dashboard-fade-in 350ms ease both;
}

/* Welcome mascot: animated SVG by default, static PNG when motion is reduced */
.mascot-greeting-static {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .card-fade-in,
    .animate-highlight,
    .animate-empty-enter,
    .skeleton,
    .dashboard-entrance {
        animation: dashboard-fade-in 200ms ease both;
    }

    .plan-item-anim,
    .plan-item-anim.is-entering,
    .plan-item-anim.is-entering > .plan-item-anim-inner,
    .plan-item-anim.is-exiting,
    .plan-item-anim.is-exiting:first-child + .plan-item-anim:not(.is-exiting) {
        transition: none;
    }

    .plan-item-anim.is-exiting {
        opacity: 0;
    }

    .greeting-word {
        animation-delay: 0ms !important;
        animation-name: dashboard-fade-in;
    }

    .greeting-subtitle-enter,
    .greeting-term-enter {
        animation-duration: 200ms;
    }

    .mascot-greeting-motion {
        display: none;
    }

    .mascot-greeting-static {
        display: block;
    }

    .school-confirmation-enter {
        animation: dashboard-fade-in 200ms ease both;
    }

    /* Disable slide-in/out for the handle during page transitions */
    ::view-transition-old(slide-out-panel),
    ::view-transition-new(slide-out-panel) {
        animation: none;
        opacity: 0;
    }

    .dashboard-spinner {
        animation: none;
        opacity: 1;
        transform: scale(1);
    }

    .dashboard-spinner-fading {
        transition: none;
    }
}

/* ── Secondary Items: success checkmark entrance (cross-fade overlay) ── */

@keyframes secondary-check-enter {
    0% {
        opacity: 0;
        transform: scale(0.6);
    }
    60% {
        opacity: 1;
        transform: scale(1.1);
    }
    100% {
        transform: scale(1);
    }
}

/* Delays trail .secondary-badge-settle so the check lands on a badge that has already
   filled — a white check drawn over the half-faded disc would read as a glitch.
   No display here: the badge centres both icons with flex, and an inline-block would
   override that and drop the check into the corner of its box. */
.secondary-check-icon {
    animation: secondary-check-enter 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
    animation-delay: 300ms;
}

.secondary-check-icon-staggered {
    animation: secondary-check-enter 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
    animation-delay: 380ms;
}

/* ── School confirmation panel entrance ── */

@keyframes school-confirmation-enter {
    from {
        opacity: 0;
        transform: scale(0.96) translateY(4px);
    }
    60% {
        transform: scale(1.02) translateY(0);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

.school-confirmation-enter {
    opacity: 0;
    animation: school-confirmation-enter 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* ── Select-school atmospheric wisps — mirrors Home.razor motif ── */

.select-school-botanical {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
}

.select-school-wisp {
    position: absolute;
    animation: select-school-wisp-drift var(--drift-duration) ease-in-out infinite;
    animation-delay: var(--delay);
}

.select-school-wisp-1 {
    --drift-duration: 24s;
    --delay: 0s;
    width: 600px;
    bottom: 30%;
    left: 45%;
}

.select-school-wisp-2 {
    --drift-duration: 28s;
    --delay: -6s;
    width: 700px;
    bottom: 15%;
    left: 38%;
}

.select-school-wisp-3 {
    --drift-duration: 22s;
    --delay: -12s;
    width: 550px;
    bottom: 50%;
    left: 50%;
}

@keyframes select-school-wisp-drift {
    0% { transform: translate(0, 0) rotate(0deg); }
    25% { transform: translate(30px, -25px) rotate(2.5deg); }
    50% { transform: translate(-20px, 15px) rotate(-1.5deg); }
    75% { transform: translate(25px, -30px) rotate(2deg); }
    100% { transform: translate(0, 0) rotate(0deg); }
}

@media (max-width: 1023px) {
    .select-school-wisp-1 {
        width: 350px;
        bottom: 20%;
        left: 15%;
    }

    .select-school-wisp-2 {
        width: 420px;
        bottom: 5%;
        left: 10%;
    }

    .select-school-wisp-3 {
        width: 320px;
        bottom: 40%;
        left: 5%;
    }
}

@media (prefers-reduced-motion: reduce) {
    .select-school-wisp {
        animation: none;
    }
}

/* ── Secondary Items: base icon exit (cross-fade out) ── */

@keyframes secondary-icon-exit {
    to {
        opacity: 0;
        transform: scale(0.8);
    }
}

.secondary-icon-exit {
    animation: secondary-icon-exit 250ms ease-out forwards;
    animation-delay: 150ms;
}

.secondary-icon-exit-staggered {
    animation: secondary-icon-exit 250ms ease-out forwards;
    animation-delay: 230ms;
}

/* ── Secondary Items: badge settling from its open disc to the filled "done" state ── */

@keyframes secondary-badge-settle {
    from {
        background-color: var(--color-surface-raised);
    }
    to {
        background-color: var(--color-primary);
    }
}

.secondary-badge-settle {
    animation: secondary-badge-settle 300ms ease-out both;
}

.secondary-badge-settle-staggered {
    animation: secondary-badge-settle 300ms ease-out both;
    animation-delay: 80ms;
}

/* ── Secondary Items: success checkmark pop (always-visible pulse) ── */

@keyframes secondary-check-pop {
    0% {
        transform: scale(1);
    }
    40% {
        transform: scale(1.25);
    }
    100% {
        transform: scale(1);
    }
}

.secondary-check-pop {
    display: inline-block;
    animation: secondary-check-pop 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
    animation-delay: 300ms;
}

/* ── Primary Action: holiday week icon & name ── */

.primary-holiday-icon {
    display: inline-block;
    transform-origin: center bottom;
    animation: secondary-holiday-sway 800ms cubic-bezier(0.25, 1, 0.5, 1) both;
    animation-delay: 300ms;
}

/* ── Secondary Items: holiday icon gentle sway ── */

@keyframes secondary-holiday-sway {
    0% {
        transform: rotate(0deg);
    }
    20% {
        transform: rotate(12deg);
    }
    40% {
        transform: rotate(-8deg);
    }
    60% {
        transform: rotate(5deg);
    }
    80% {
        transform: rotate(-2deg);
    }
    100% {
        transform: rotate(0deg);
    }
}

/* No display here either — the badge centres this with flex, and an inline-block would
   override that and let the glyph ride its own baseline. */
.secondary-holiday-icon {
    transform-origin: center bottom;
    animation: secondary-holiday-sway 800ms cubic-bezier(0.25, 1, 0.5, 1) both;
    animation-delay: 300ms;
}

@media (prefers-reduced-motion: reduce) {
    .secondary-check-icon,
    .secondary-check-icon-staggered,
    .secondary-check-pop,
    .secondary-icon-exit,
    .secondary-icon-exit-staggered,
    .secondary-badge-settle,
    .secondary-badge-settle-staggered,
    .secondary-holiday-icon,
    .primary-holiday-icon {
        animation: none;
    }

    /* Without the animation there is no fill to hold the cross-fade's end state, so pin it:
       the retiring icon stays hidden and the check stays shown. */
    .secondary-icon-exit,
    .secondary-icon-exit-staggered {
        opacity: 0;
    }

    .secondary-check-icon,
    .secondary-check-icon-staggered {
        opacity: 1;
    }

}

/* ── Primary action progress meter ── */

/* Grows the fill out from nothing on entrance. Only a from-keyframe is declared, so the end
   of the animation is whatever width the element is actually set to - which is what lets the
   bar carry its true width in its inline style and treat the growth as pure decoration.
   Without the animation class the bar simply appears at its value, which is why this is the
   one part of the meter that lives in CSS rather than a utility. */
@keyframes dashboard-progress-fill {
    from {
        width: 0;
    }
}

.dashboard-progress-fill {
    animation: dashboard-progress-fill 800ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

@media (prefers-reduced-motion: reduce) {
    .dashboard-progress-fill {
        animation: none;
    }
}

/* ── All-Clear Celebration State ── */

@keyframes all-clear-appear {
    from {
        opacity: 0;
        transform: scale(0.85);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

.all-clear-icon-wrapper {
    animation: all-clear-appear 500ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
    display: inline-block;
}

@media (prefers-reduced-motion: reduce) {
    .all-clear-icon-wrapper {
        animation: none;
    }
}

/* ── Dashboard Season Accent Line ── */

.dashboard-season-line {
    height: 2px;
    border: none;
    border-radius: 1px;
}

.dashboard-season-line.season-spring {
    background: linear-gradient(90deg, oklch(88% 0.07 145), oklch(90% 0.06 85), transparent 80%);
}

.dashboard-season-line.season-summer {
    background: linear-gradient(90deg, oklch(90% 0.07 80), oklch(92% 0.06 60), transparent 80%);
}

.dashboard-season-line.season-autumn {
    background: linear-gradient(90deg, oklch(86% 0.07 50), oklch(88% 0.06 30), transparent 80%);
}

.dashboard-season-line.season-winter {
    background: linear-gradient(90deg, oklch(88% 0.04 220), oklch(90% 0.03 245), transparent 80%);
}

.dark .dashboard-season-line.season-spring {
    background: linear-gradient(90deg, oklch(30% 0.05 145), oklch(28% 0.04 85), transparent 80%);
}

.dark .dashboard-season-line.season-summer {
    background: linear-gradient(90deg, oklch(30% 0.05 80), oklch(28% 0.04 60), transparent 80%);
}

.dark .dashboard-season-line.season-autumn {
    background: linear-gradient(90deg, oklch(28% 0.05 50), oklch(26% 0.04 30), transparent 80%);
}

.dark .dashboard-season-line.season-winter {
    background: linear-gradient(90deg, oklch(26% 0.03 220), oklch(24% 0.02 245), transparent 80%);
}

/* ── Dashboard Year Overview Weeks ── */

.dashboard-week {
    display: block;
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 0.125rem;
    transition: background-color 150ms ease;
}

.dashboard-week--planned {
    background: var(--color-primary);
    opacity: 0.8;
}

.dashboard-week--current {
    position: relative;
}

.dashboard-week--current::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 5px solid transparent;
    border-bottom-color: var(--color-primary);
}

.dashboard-week--holiday {
    background: var(--color-accent);
    opacity: 0.7;
}

.dashboard-week--unplanned {
    background: var(--color-surface-alt);
}

.dashboard-week--partial-holiday {
    background: linear-gradient(135deg, var(--color-accent) 50%, var(--color-surface-alt) 50%);
    opacity: 0.7;
}

.dashboard-week--planned-partial-holiday {
    background: linear-gradient(
        135deg,
        var(--color-primary) 50%,
        var(--color-accent) 50%
    );
    opacity: 0.8;
}

.dashboard-week--clickable {
    cursor: pointer;
    border: none;
    padding: 0;
}

.dashboard-week--clickable:hover {
    filter: brightness(1.2);
}

.dashboard-week--clickable:focus-visible {
    outline: 2px solid var(--color-primary);
    outline-offset: 1px;
}

/* ── Dashboard Week Tooltip ── */

.dashboard-week-tooltip {
    position: absolute;
    bottom: 100%;
    left: 50%;
    margin-bottom: 0.5rem;
    transform: translateX(-50%) translateY(2px);
    opacity: 0;
    pointer-events: none;
    white-space: nowrap;
    z-index: 50;
    padding: 0.25rem 0.5rem;
    font-size: 0.75rem;
    line-height: 1rem;
    color: var(--color-surface);
    background: var(--color-label-strong);
    border-radius: 0.25rem;
    transition: opacity 150ms ease, transform 150ms ease;
}

.dashboard-week-tooltip::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 4px solid transparent;
    border-top-color: var(--color-label-strong);
}

.group:hover .dashboard-week-tooltip {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
    .dashboard-week,
    .dashboard-week-tooltip {
        transition: none;
    }
}

/* ── School Year Overview Cells ── */

.year-cell {
    /* Both the plain fills and the diagonal split halves read these, so a holiday
       cell and the holiday half of a split cell always resolve to the same colour. */
    --year-cell-holiday-fill: color-mix(in oklab, var(--color-accent) 70%, var(--color-surface));
    /* Partial is a faded sibling of the primary — same hue, same lightness as before, but the
       primary's very high chroma (0.3) is halved first so the tint reads as a dimmed green
       rather than a vivid mint that appears nowhere else. Still mixed toward surface-alt, so it
       lightens with the light theme and darkens with the dark theme, staying a muted primary in both. */
    --year-cell-partial-fill: color-mix(
        in oklab,
        oklch(from var(--color-primary) l calc(c * 0.5) h) 40%,
        var(--color-surface-alt)
    );

    display: block;
    width: 1.25rem;
    height: 1.25rem;
    border-radius: 0.1875rem;
    flex-shrink: 0;
}

.year-cell--planned {
    background: var(--color-primary);
}

.year-cell--partial {
    background: var(--year-cell-partial-fill);
}

.year-cell--unplanned {
    background: var(--color-surface-alt);
}

.year-cell--holiday {
    background: var(--year-cell-holiday-fill);
}

/* Split cells: diagonal gradient for partial holiday + plan state */
.year-cell--split-planned {
    background: linear-gradient(135deg, var(--year-cell-holiday-fill) 50%, var(--color-primary) 50%);
}

.year-cell--split-partial {
    background: linear-gradient(
        135deg,
        var(--year-cell-holiday-fill) 50%,
        var(--year-cell-partial-fill) 50%
    );
}

.year-cell--split-unplanned {
    background: linear-gradient(135deg, var(--year-cell-holiday-fill) 50%, var(--color-surface-alt) 50%);
}

/* Current week marker */
.year-cell[data-current="true"] {
    outline: 2px solid var(--color-primary);
    outline-offset: 1px;
}

/* Fortnight container: inline-flex, never breaks across lines. The gap matches the
   parent row's gap-1 so a fortnight pair keeps the same rhythm as ordinary cells. */
.year-cell-fortnight {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    flex-wrap: nowrap;
}

/* Fortnight connector bar, absolutely positioned into the gap so it bridges the two
   cells without displacing them. --conn-index is the 1-based index of the gap it fills;
   each cell occupies 1.25rem plus the 0.25rem gap that follows it. */
.year-cell-connector {
    position: absolute;
    top: 50%;
    left: calc(var(--conn-index) * 1.5rem - 0.25rem);
    width: 0.25rem;
    height: 0.25rem;
    margin-top: -0.125rem;
    background: var(--color-primary);
}

/* ── Year Overview Entrance Animations ── */

@keyframes year-overview-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes year-overview-week-enter {
    from { opacity: 0; transform: scale(0); }
    to { opacity: 1; transform: scale(1); }
}

.year-overview-fade-enter {
    animation: year-overview-fade-in 300ms ease both;
}

.year-overview-week-enter {
    animation: year-overview-week-enter 250ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

@media (prefers-reduced-motion: reduce) {
    .year-overview-fade-enter,
    .year-overview-week-enter {
        animation: none;
    }
}

/* ── Week popover (native Popover API) ──
   The shell is a top-layer popover element, so it is positioned against the viewport
   regardless of transformed ancestors. It carries no visual chrome itself — that lives on
   the inner card — so the caret can sit just outside the card's edge. */
.week-popover-shell {
    width: 460px;
    max-width: calc(100vw - 1rem);
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    overflow: visible;
    color: inherit;
}

.week-popover-shell:focus-visible {
    outline: none;
}

.week-popover-shell::backdrop {
    background: transparent;
}

/* Desktop anchored: a gentle fade + rise on open, played in reverse on close. Opacity and
   transform don't disturb the inline top/left placement; allow-discrete on display/overlay
   holds the element in the top layer long enough for the fade-out to finish. */
.week-popover-shell[data-variant="anchored"] {
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 150ms ease,
                transform 150ms cubic-bezier(0.22, 1, 0.36, 1),
                overlay 150ms allow-discrete,
                display 150ms allow-discrete;
}

.week-popover-shell[data-variant="anchored"]:popover-open {
    opacity: 1;
    transform: translateY(0);
}

@starting-style {
    .week-popover-shell[data-variant="anchored"]:popover-open {
        opacity: 0;
        transform: translateY(4px);
    }
}

/* Reduced motion: keep the fade (not vestibular), drop the rise. */
@media (prefers-reduced-motion: reduce) {
    .week-popover-shell[data-variant="anchored"] {
        transform: none;
    }
}

.week-popover-card {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    max-height: 360px;
    overflow: hidden;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border-default);
    border-radius: 0.75rem;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

/* Caret: a rotated square sharing the card's fill and border. It sits in front of the card
   so its inner half covers the card's border seam and its outer half forms the pointed tip. */
.week-popover-caret {
    position: absolute;
    z-index: 2;
    left: var(--caret-left, 50%);
    width: 12px;
    height: 12px;
    background: var(--color-surface-raised);
    transform: translateX(-50%) rotate(45deg);
}

.week-popover-shell[data-caret-side="bottom"] .week-popover-caret {
    bottom: -6px;
    border-right: 1px solid var(--color-border-default);
    border-bottom: 1px solid var(--color-border-default);
}

.week-popover-shell[data-caret-side="top"] .week-popover-caret {
    top: -6px;
    border-left: 1px solid var(--color-border-default);
    border-top: 1px solid var(--color-border-default);
}

/* Small screens: a bottom drawer that slides up, sized to its content and capped so a busy
   week scrolls internally rather than covering the whole screen. The native [popover] toggles
   display:none, so the slide-in AND slide-out are driven by @starting-style plus
   transition-behavior:allow-discrete on display/overlay, which holds the element in the top
   layer until the transition finishes. Requires Safari 17.4+; older browsers open and close it
   instantly (still fully functional, still light-dismissable). */
.week-popover-shell[data-variant="drawer"] {
    inset: auto 0 0 0;
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: 85dvh;
    margin: 0;
    overflow: visible;
    transform: translateY(100%);
    opacity: 0;
    transition: transform 300ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 200ms ease,
                overlay 300ms allow-discrete,
                display 300ms allow-discrete;
}

.week-popover-shell[data-variant="drawer"]:popover-open {
    transform: translateY(0);
    opacity: 1;
}

@starting-style {
    .week-popover-shell[data-variant="drawer"]:popover-open {
        transform: translateY(100%);
        opacity: 0;
    }
}

/* Dimmed backdrop, fading in and out in step with the drawer. */
.week-popover-shell[data-variant="drawer"]::backdrop {
    background: var(--color-surface-overlay);
    opacity: 0;
    transition: opacity 300ms ease,
                overlay 300ms allow-discrete,
                display 300ms allow-discrete;
}

.week-popover-shell[data-variant="drawer"]:popover-open::backdrop {
    opacity: 1;
}

@starting-style {
    .week-popover-shell[data-variant="drawer"]:popover-open::backdrop {
        opacity: 0;
    }
}

.week-popover-shell[data-variant="drawer"] .week-popover-card {
    height: 100%;
    max-height: 85dvh;
    padding-top: 0.75rem;   /* clearance for the grabber handle */
    border-bottom: 0;
    border-radius: 1rem 1rem 0 0;
    box-shadow: 0 -12px 40px -12px rgba(0, 0, 0, 0.35);
}

/* Grabber handle: a quiet pill signalling the drawer is dismissible. */
.week-popover-shell[data-variant="drawer"] .week-popover-card::before {
    content: "";
    position: absolute;
    top: 0.4rem;
    left: 50%;
    z-index: 3;
    width: 2.25rem;
    height: 0.25rem;
    transform: translateX(-50%);
    border-radius: 9999px;
    background: var(--color-border-strong);
}

.week-popover-shell[data-variant="drawer"] .week-popover-caret {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    .week-popover-shell[data-variant="drawer"] {
        transform: none;
        transition: none;
    }
    .week-popover-shell[data-variant="drawer"]::backdrop {
        transition: none;
    }
}

/* ── Menu popover (native Popover API) ──
   A lightweight sibling of the week popover, used for anchored action menus. The browser
   opens and closes it from the trigger's popovertarget; popover.js only places it. It has
   no caret and no drawer variant — it stays anchored below its trigger at every width. */
.menu-popover-shell {
    min-width: 12rem;
    max-width: calc(100vw - 1rem);
    margin: 0;
    padding: 0;
    border: 0;
    background: transparent;
    overflow: visible;
    color: inherit;
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 150ms ease,
                transform 150ms cubic-bezier(0.22, 1, 0.36, 1),
                overlay 150ms allow-discrete,
                display 150ms allow-discrete;
}

.menu-popover-shell:focus-visible {
    outline: none;
}

.menu-popover-shell::backdrop {
    background: transparent;
}

.menu-popover-shell:popover-open {
    opacity: 1;
    transform: translateY(0);
}

@starting-style {
    .menu-popover-shell:popover-open {
        opacity: 0;
        transform: translateY(4px);
    }
}

/* Reduced motion: keep the fade (not vestibular), drop the rise. The open and
   @starting-style rules above are (0,2,0), so they must be overridden at matching
   specificity — and transform leaves the transition list so nothing animates it. */
@media (prefers-reduced-motion: reduce) {
    .menu-popover-shell {
        transform: none;
        transition: opacity 150ms ease,
                    overlay 150ms allow-discrete,
                    display 150ms allow-discrete;
    }

    .menu-popover-shell:popover-open {
        transform: none;
    }

    @starting-style {
        .menu-popover-shell:popover-open {
            transform: none;
        }
    }
}

.menu-popover-card {
    display: flex;
    flex-direction: column;
    padding: 0.25rem 0;
    overflow: hidden;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border-default);
    border-radius: 0.75rem;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

/* ── Blazor Reconnection Modal ── */

#components-reconnect-modal {
    display: none;
}

#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
    display: block;
}

.reconnect-overlay {
    position: fixed;
    inset: 0;
    background: var(--color-surface-overlay);
    backdrop-filter: blur(2px);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 70;
    animation: reconnect-fade-in 300ms ease both;
}

.reconnect-card {
    width: 90%;
    max-width: 28rem;
    background: var(--color-surface-raised);
    border-radius: 0.75rem;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, .1), 0 8px 10px -6px rgba(0, 0, 0, .1);
    overflow: hidden;
    animation: reconnect-card-enter 350ms cubic-bezier(0.25, 1, 0.5, 1) both;
    animation-delay: 50ms;
}

.reconnect-state {
    display: none;
}

#components-reconnect-modal.components-reconnect-show .reconnect-state--show,
#components-reconnect-modal.components-reconnect-failed .reconnect-state--failed,
#components-reconnect-modal.components-reconnect-rejected .reconnect-state--rejected {
    display: block;
}

.reconnect-body {
    padding: 1.5rem;
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

.reconnect-icon-box {
    width: 3rem;
    height: 3rem;
    flex-shrink: 0;
    border-radius: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.5rem;
}

.reconnect-icon-box--info {
    background: var(--color-primary-subtle);
    color: var(--color-primary);
}

.reconnect-icon-box--warning {
    background: var(--color-warning-subtle);
    color: var(--color-warning);
}

.reconnect-icon-box--error {
    background: var(--color-error-subtle);
    color: var(--color-error);
}

.reconnect-spinner {
    animation: reconnect-spin 1.2s linear infinite;
}

.reconnect-heading {
    font-size: 1.125rem;
    line-height: 1.44;
    font-weight: 600;
    letter-spacing: -0.005em;
    color: var(--color-label-primary);
    margin: 0 0 0.25rem;
}

.reconnect-message {
    font-size: 0.875rem;
    line-height: 1.43;
    font-weight: 400;
    color: var(--color-label-secondary);
    margin: 0;
}

.reconnect-footer {
    padding: 0.75rem 1.5rem;
    background: var(--color-surface-alt);
    border-top: 1px solid var(--color-border-subtle);
    border-radius: 0 0 0.75rem 0.75rem;
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
}

.reconnect-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border-radius: 0.375rem;
    transition: all 200ms ease-out;
    padding: 0.5rem 1rem;
    font-size: 0.875rem;
    line-height: 1.43;
    font-weight: 500;
    font-family: 'Plus Jakarta Sans', sans-serif;
    border: none;
}

.reconnect-btn:active {
    transform: scale(0.97);
}

.reconnect-btn--primary {
    background: var(--color-primary);
    color: var(--color-static-white);
}

.reconnect-btn--primary:hover {
    background: var(--color-primary-hover);
}

.reconnect-btn--primary:active {
    background: var(--color-primary-active);
}

.reconnect-btn--secondary {
    background: var(--color-surface);
    color: var(--color-label-primary);
    border: 1px solid var(--color-border-default);
}

.reconnect-btn--secondary:hover {
    background: var(--color-surface-alt);
    border-color: var(--color-border-strong);
}

@keyframes reconnect-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes reconnect-card-enter {
    from {
        opacity: 0;
        transform: scale(0.95) translateY(8px);
    }
    to {
        opacity: 1;
        transform: scale(1) translateY(0);
    }
}

@keyframes reconnect-spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .reconnect-overlay,
    .reconnect-card {
        animation: none;
    }

    .reconnect-spinner {
        animation: reconnect-spin 2.4s linear infinite;
    }
}
