/* ==========================================================================
   Fonts — self-hosted Inter (variable), no external font CDN
   ========================================================================== */

@font-face {
    font-family: 'Inter';
    src: url('fonts/InterVariable.woff2') format('woff2');
    font-weight: 100 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Inter';
    src: url('fonts/InterVariable-Italic.woff2') format('woff2');
    font-weight: 100 900;
    font-style: italic;
    font-display: swap;
}

/* ==========================================================================
   Design tokens
   ========================================================================== */

:root {
    /* Light theme — palette drawn from img/sam.png: off-white stripes,
       cream skin, deep plum outline, chestnut hair, and the periwinkle
       backdrop, so the page's neutrals and accent tie directly to the
       illustration rather than just approximating its colors. */
    --color-bg: #F7F1DD;
    /* A generic soft off-white, not one of the sampled palette swatches —
       close to white but without the stark white-on-cream jump, and still
       clearly distinct from --color-bg's more saturated cream. */
    --color-surface: #FCFAF3;
    --color-surface-muted: #F2E2B7;
    --color-text: #241820;
    --color-text-muted: #71382D;
    --color-heading: #4A5AC4;
    --color-heading-hover: #3A47A0;
    --color-heading-contrast: #FFFFFF;
    /* Burgundy shirt stripe from img/sam.png's color palette. */
    --color-name: #8F174D;
    --color-border: #D5C7A1;
    /* Nested tiles-on-a-card (contact icons) blend into the card in light
       mode — matches --color-surface, unlike dark mode where they get
       their own visible container. */
    --color-tile: #FCFAF3;
    --color-accent: #91A4F2;
    --color-accent-hover: #748BE5;
    --color-accent-contrast: #1B2140;
    --color-focus-ring: rgba(145, 164, 242, 0.35);

    /* Type */
    --font-sans: 'Inter', -apple-system, 'Segoe UI', sans-serif;
    --font-size-sm: 0.875rem;
    --font-size-base: 1rem;
    --font-size-lg: 1.15rem;
    --font-size-xl: 1.5rem;
    --font-size-2xl: 2.25rem;
    --font-size-3xl: 3rem;
    --line-height-tight: 1.2;
    --line-height-normal: 1.6;

    /* Spacing scale */
    --space-1: 0.25rem;
    --space-2: 0.5rem;
    --space-3: 0.75rem;
    --space-4: 1rem;
    --space-5: 1.5rem;
    --space-6: 2rem;
    --space-7: 3rem;
    --space-8: 4.5rem;

    /* Shape */
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 18px;
    --shadow-sm: 0 1px 2px rgba(36, 24, 32, 0.06), 0 1px 3px rgba(36, 24, 32, 0.08);
    --shadow-md: 0 4px 12px rgba(36, 24, 32, 0.08), 0 2px 4px rgba(36, 24, 32, 0.06);

    color-scheme: light;
}

:root[data-theme='dark'] {
    /* Dark theme — the deep plum outline from img/sam.png anchors the
       background here, same as it anchors --color-text in light mode,
       so both themes' "ink" traces back to the same illustration color. */
    --color-bg: #241820;
    --color-surface: #2E1F28;
    --color-surface-muted: #3A2732;
    --color-text: #F7F1DD;
    --color-text-muted: #B6A290;
    --color-heading: var(--color-accent);
    --color-heading-hover: var(--color-accent-hover);
    /* Lightened tint of the same shirt-stripe hue — the exact shade
       (#8F174D) is too close to this dark background to read. */
    --color-name: #C17F9D;
    --color-heading-contrast: var(--color-accent-contrast);
    --color-border: #4A3040;
    /* Distinct from --color-surface here — dark mode gets a visible tile
       container, unlike light mode where it blends into the card. */
    --color-tile: var(--color-surface-muted);
    --color-accent: #AEBCFF;
    --color-accent-hover: #C7D1FF;
    --color-accent-contrast: #1B2140;
    --color-focus-ring: rgba(174, 188, 255, 0.4);
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.3);
    --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35), 0 2px 4px rgba(0, 0, 0, 0.3);

    color-scheme: dark;
}

/* ==========================================================================
   Base
   ========================================================================== */

html {
    scroll-behavior: smooth;
}

/* Section-nav (SectionNav.razor) click targets. These ids live on
   elements in different scoped components (BioHeader, Home, Contact),
   so the offset has to live here rather than in any one .razor.css —
   keeps each section's top edge clear of the fixed nav bar when
   scrolled to. */
#hero,
#my-journey,
#tech-stack,
#projects,
#hobbies,
#contact {
    scroll-margin-top: var(--space-8);
}

html, body {
    font-family: var(--font-sans);
    background-color: var(--color-bg);
    color: var(--color-text);
    font-size: var(--font-size-base);
    line-height: var(--line-height-normal);
    transition: background-color 150ms ease, color 150ms ease;
}

h1, h2, h3, h4, h5, h6 {
    font-weight: 600;
    line-height: var(--line-height-tight);
    letter-spacing: -0.01em;
}

h1:focus {
    outline: none;
}

a, .btn-link {
    color: var(--color-accent);
    text-decoration: none;
}

a:hover, .btn-link:hover {
    color: var(--color-accent-hover);
    text-decoration: underline;
}

.content {
    padding-top: 1.1rem;
}

/* .page and .app-content live here rather than in MainLayout.razor.css —
   see the comment there. */

.page {
    position: relative;
    display: flex;
    flex-direction: column;
}

.app-content {
    padding-block: var(--space-6);
}

/* The project screenshot carousels (.project-showcase__media) center their
   .carousel child with flex, which never forces that child to stretch to
   fill the available width — it shrink-to-fits instead. For a percentage-
   width img inside, browsers fall back to the img's own intrinsic aspect
   ratio (combined with its resolved height) to compute that shrink-to-fit
   width, so the carousel's rendered width silently changes based on
   whichever slide's image is active, instead of staying fixed to its
   container. Forcing an explicit 100% here breaks that shrink-to-fit
   calculation so the carousel always fills its box consistently.
   Global rather than scoped: .carousel/.carousel-inner are rendered deep
   inside Blazorise's Carousel component, not literal HTML in any of our
   files, so a scoped rule here wouldn't reach them. */
.project-showcase__media .carousel,
.project-showcase__media .carousel-inner {
    width: 100%;
    height: 100%;
}

/* Bootstrap's carousel prev/next icons are a hardcoded white SVG, meant for
   a dark carousel background. The project screenshot carousels sit on
   --color-surface, which is dark in dark mode (icon stays visible) but
   white in light mode (icon disappears) — invert just for light mode.
   Global rather than scoped: these classes are rendered deep inside
   Blazorise's Carousel component, not literal HTML in any of our files. */
:root:not([data-theme='dark']) .carousel-control-prev-icon,
:root:not([data-theme='dark']) .carousel-control-next-icon {
    filter: invert(1);
}

/* Minimal button system — used in place of default Bootstrap button colors */
.btn-accent {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: var(--color-heading);
    color: var(--color-heading-contrast);
    border: 1px solid var(--color-heading);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-5);
    font-weight: 500;
    transition: background-color 150ms ease, border-color 150ms ease;
}

.btn-accent:hover {
    background-color: var(--color-heading-hover);
    border-color: var(--color-heading-hover);
    color: var(--color-heading-contrast);
    text-decoration: none;
}

.btn-ghost {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background-color: transparent;
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-5);
    font-weight: 500;
    transition: border-color 150ms ease, background-color 150ms ease;
}

.btn-ghost:hover {
    border-color: var(--color-accent);
    background-color: var(--color-surface-muted);
    color: var(--color-text);
    text-decoration: none;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus,
.btn-accent:focus, .btn-ghost:focus {
    box-shadow: 0 0 0 0.2rem var(--color-focus-ring);
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

/* Contact modal (Get in Touch). Global rather than scoped: ContactModal.razor's
   markup is entirely Blazorise components (Modal/ModalContent/etc.), so no
   element in the rendered modal ever carries that component's CSS-isolation
   scope attribute — a ::deep rule there only matches by the accident of which
   wrapper divs happen to sit in between, so it can silently apply to some
   fields and not others. See the carousel-icon rule above for the same reason. */
.contact-modal__content {
    background-color: var(--color-surface);
    color: var(--color-text);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
}

/* Match .contact-modal__body's horizontal padding — Bootstrap's default
   modal-header padding (1rem) is narrower, which left the close button
   inset less than the form fields below it. */
.contact-modal__header {
    border-bottom-color: var(--color-border);
    padding-left: var(--space-5);
    padding-right: var(--space-5);
}

.contact-modal__title {
    color: var(--color-text);
    font-size: var(--font-size-lg);
    font-weight: 600;
}

.contact-modal__body {
    padding: var(--space-5);
}

/* Bootstrap's default .btn-close padding (0.25em) makes for a cramped
   click target sitting right against the modal's edge — give it more room.
   Margin stays at 0 (only auto on the left, to keep it pushed to the flex
   row's right edge) so the button's own right edge sits flush with the
   header's content edge — the same edge .contact-modal__body's padding
   aligns to, so it lines up with the field inputs below it. */
.contact-modal__content .btn-close {
    padding: var(--space-3);
    margin: 0 0 0 auto;
}

/* Bootstrap's .btn-close icon is a dark X meant for light backgrounds —
   invert it in dark mode so it stays visible on the dark modal surface. */
:root[data-theme='dark'] .contact-modal__content .btn-close {
    filter: invert(1);
}

/* Image lightbox (project screenshots). Global for the same reason as the
   contact modal above — ImageLightbox.razor's markup is entirely Blazorise
   components, so a scoped/::deep rule there can't reliably reach it. */
/* .image-lightbox__content is the Class passed to <ModalContent>, which
   Blazorise renders as .modal-content — the inner card, not the outer
   .modal-dialog that Bootstrap actually centers via margin:auto. Widening
   .modal-content itself just overflows past its still-constrained parent
   dialog instead of growing it, which is what made this open off-center.
   :has() lets us reach the actual dialog wrapper instead. */
.modal-dialog:has(.image-lightbox__content) {
    max-width: 95vw;
}

.image-lightbox__content {
    background-color: transparent;
    border: none;
    box-shadow: none;
}

.image-lightbox__header {
    border-bottom: none;
    padding: var(--space-3) var(--space-3) 0;
}

.image-lightbox__body {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 0 var(--space-3) var(--space-3);
}

.image-lightbox__image {
    max-width: 100%;
    max-height: 90vh;
    border-radius: var(--radius-md);
}

.image-lightbox__content .btn-close {
    padding: var(--space-3);
    margin: 0 0 0 auto;
}

:root[data-theme='dark'] .image-lightbox__content .btn-close {
    filter: invert(1);
}

.contact-form {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.contact-form__row {
    display: flex;
    gap: var(--space-4);
}

.contact-form__row .contact-form__field {
    flex: 1;
}

.contact-form__field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}

.contact-form__field label {
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-muted);
}

.contact-form__input {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background-color: var(--color-bg);
    color: var(--color-text);
    padding: var(--space-2) var(--space-3);
    font-family: inherit;
    font-size: var(--font-size-base);
    resize: none;
}

.contact-form__input:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 0.2rem var(--color-focus-ring);
}

.contact-form > button[type='submit'] {
    align-self: flex-start;
    margin-top: var(--space-2);
}

@media (max-width: 640px) {
    .contact-form__row {
        flex-direction: column;
    }
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
