/* Google Fonts Imports (base fallback family - each theme file imports its own display font on top of this) */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap');

/* ==========================================================================
   BASE / FALLBACK VARIABLES
   These are safe fallbacks only. Every [data-ui-style] theme file
   (static/css/themes/*.css) overrides all of these with its OWN distinct
   palette, fonts and shadow system, scoped strictly under its own
   [data-ui-style="..."] selector so the five themes never bleed into each
   other. This file only owns layout/structure — never final color values.
   ========================================================================== */
:root {
    --font-primary: 'Outfit', 'Inter', sans-serif;
    --font-mono: 'Fira Code', monospace;

    /* Fallback accent (overridden per-theme) */
    --accent-color: #10b981;
    --accent-glow: rgba(16, 185, 129, 0.45);
    --accent-light: #d1fae5;
    --accent-color-2: #06b6d4;
    --accent-color-3: #8b5cf6;
    --dock-bg: rgba(26, 34, 52, 0.75);
    --dock-border: rgba(255, 255, 255, 0.12);

    /* Transition speeds */
    --transition-fast: 0.2s ease;
    --transition-normal: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
    --transition-slow: 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}

/* Light Theme Variables */
[data-theme="light"] {
    --bg-primary: #e0e5ec;
    --bg-secondary: #f0f3f6;
    --text-primary: #2d3748;
    --text-secondary: #718096;
    --text-muted: #a0aec0;
    
    /* Neumorphic Shadows */
    --nm-shadow-flat-dark: 9px 9px 16px #a3b1c6;
    --nm-shadow-flat-light: -9px -9px 16px #ffffff;
    --nm-shadow-inset-dark: inset 6px 6px 10px #b8c6d9, inset -6px -6px 10px #ffffff;
    
    /* Glassmorphism */
    --glass-bg: rgba(255, 255, 255, 0.45);
    --glass-border: rgba(255, 255, 255, 0.25);
    --glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.08);

    /* GitHub contribution graph — light palette, empty cell kept dark enough
       to stay visible against the light neumorphic background */
    --contrib-level-0: rgba(45, 55, 72, 0.12);
    --contrib-level-1: #9be9a8;
    --contrib-level-2: #40c463;
    --contrib-level-3: #30a14e;
    --contrib-level-4: #216e39;
    --skeleton-shine: rgba(45, 55, 72, 0.08);
}

/* Dark Theme Variables (Default) */
[data-theme="dark"] {
    --bg-primary: #121824;
    --bg-secondary: #1a2234;
    --text-primary: #f7fafc;
    --text-secondary: #cbd5e0;
    --text-muted: #718096;
    
    /* Neumorphic Shadows */
    --nm-shadow-flat-dark: 8px 8px 16px #0b0f17;
    --nm-shadow-flat-light: -8px -8px 16px #1d273b;
    --nm-shadow-inset-dark: inset 6px 6px 12px #0a0d14, inset -6px -6px 12px #1e283c;
    
    /* Glassmorphism */
    --glass-bg: rgba(26, 34, 52, 0.65);
    --glass-border: rgba(255, 255, 255, 0.08);
    --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);

    /* GitHub contribution graph — dark palette, empty cell brightened up
       so it stays clearly visible against the dark card background */
    --contrib-level-0: rgba(255, 255, 255, 0.14);
    --contrib-level-1: #0e4429;
    --contrib-level-2: #006d32;
    --contrib-level-3: #26a641;
    --contrib-level-4: #39d353;
    --skeleton-shine: rgba(255, 255, 255, 0.06);
}

/* Accent Colors Overrides */
[data-accent="purple"] { --accent-color: #8b5cf6; --accent-glow: rgba(139, 92, 246, 0.45); }
[data-accent="indigo"] { --accent-color: #6366f1; --accent-glow: rgba(99, 102, 241, 0.45); }
[data-accent="rose"] { --accent-color: #f43f5e; --accent-glow: rgba(244, 63, 94, 0.45); }
[data-accent="amber"] { --accent-color: #f59e0b; --accent-glow: rgba(245, 158, 11, 0.45); }
[data-accent="cyan"] { --accent-color: #06b6d4; --accent-glow: rgba(6, 182, 212, 0.45); }

/* Global Styling */
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    scrollbar-width: thin;
    scrollbar-color: var(--accent-color) var(--bg-primary);
}

/* overflow-x lives on BOTH html and body: some mobile browsers base the
   page's horizontal pan/drag range on the <html> (root scrolling) element,
   so hiding it only on body wasn't always enough to stop the page being
   draggable sideways - and when that happens, right-anchored fixed
   elements (the AI button, `right: 20px`) get positioned relative to that
   wider phantom viewport and end up shoved off past the visible edge. */
html {
    overflow-x: hidden;
    width: 100%;
}

body {
    font-family: var(--font-primary);
    background-color: var(--bg-primary);
    color: var(--text-primary);
    min-height: 100dvh;
    width: 100%;
    overflow-x: hidden;
    transition: background-color var(--transition-slow), color var(--transition-slow);
}

/* Scrollbar Customization */
::-webkit-scrollbar {
    width: 8px;
}
::-webkit-scrollbar-track {
    background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
    background: var(--accent-color);
    border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
    background: var(--accent-glow);
}

/* Neumorphic UI Primitives */
.nm-flat {
    background: var(--bg-secondary);
    box-shadow: var(--nm-shadow-flat-dark), var(--nm-shadow-flat-light);
    border-radius: 16px;
    border: 1px solid var(--glass-border);
    transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}

.nm-flat:hover {
    transform: translateY(-4px);
    box-shadow: 12px 12px 24px var(--nm-shadow-flat-dark), -12px -12px 24px var(--nm-shadow-flat-light);
}

.nm-inset {
    background: var(--bg-primary);
    box-shadow: var(--nm-shadow-inset-dark);
    border-radius: 12px;
}

.nm-button {
    background: var(--bg-secondary);
    box-shadow: var(--nm-shadow-flat-dark), var(--nm-shadow-flat-light);
    border: none;
    cursor: pointer;
    border-radius: 12px;
    font-weight: 600;
    color: var(--text-primary);
    transition: all var(--transition-fast);
}

.nm-button:active {
    box-shadow: var(--nm-shadow-inset-dark);
}

/* Glassmorphic UI Card */
.glass-card {
    background: var(--glass-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--glass-shadow);
    border-radius: 20px;
    padding: 24px;
    transition: transform var(--transition-normal), box-shadow var(--transition-normal);
}

.glass-card:hover {
    box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.4);
}

/* Bento Grid System */
.bento-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
    max-width: 1200px;
    margin: 25px auto;
    padding: 0 16px;
}

.bento-item {
    position: relative;
    overflow: hidden;
}

/* Experience Teaser timeline (index page). The card is intentionally
   stretched to match the Media Showcase card beside it in the same bento
   row — so instead of fighting that with a fixed gap (which leaves empty
   space at the bottom when the neighbor is taller, or overflows when it's
   shorter), the entries themselves flex to fill the full card height:
   `gap` sets the minimum spacing, `justify-content: space-between`
   distributes any extra height evenly between entries so the two cards'
   bottoms always line up. */
.timeline-teaser {
    display: flex;
    flex-direction: column;
    flex: 1;
    gap: 15px;
    justify-content: space-between;
    border-left: 2px solid var(--accent-glow);
    padding-left: 15px;
    margin-left: 10px;
}

.bento-col-2 { grid-column: span 2; }
.bento-col-3 { grid-column: span 3; }
.bento-col-4 { grid-column: span 4; }
.bento-row-2 { grid-row: span 2; }

@media (max-width: 992px) {
    .bento-grid {
        grid-template-columns: repeat(2, 1fr);
    }
    .bento-col-3, .bento-col-4 {
        grid-column: span 2;
    }
}

@media (max-width: 576px) {
    .bento-grid {
        grid-template-columns: 1fr;
        gap: 12px;
    }
    .bento-col-2, .bento-col-3, .bento-col-4 {
        grid-column: span 1;
    }
}

/* ==========================================================================
   Floating Dock — hidden on desktop/tablet (the top .primary-nav already
   covers full navigation there). It only appears below the mobile
   breakpoint, where it becomes the actual bottom navbar: full width, all
   sections, icon + label, safe-area aware. See the mobile override further
   down and the [data-ui-style] theme files for per-theme decoration on
   top of this structural layout.
   ========================================================================== */
.floating-dock {
    display: none;
    position: fixed;
    bottom: 20px;
    left: 50%;
    /* translateZ(0) forces the dock onto its own compositor layer. Without
       it, mobile WebKit/Chromium can fail to paint a fixed + backdrop-filter
       element while the rest of the page's height is still settling (e.g.
       the Experience page's staggered [data-reveal] cards + tech-stack
       marquee animating in right after load) - the dock stays invisible
       until the first scroll forces a repaint. Giving it its own layer up
       front means it never depends on that first scroll to appear. */
    transform: translateX(-50%) translateZ(0);
    -webkit-transform: translateX(-50%) translateZ(0);
    will-change: transform;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
    gap: 12px;
    padding: 8px 16px;
    background: var(--dock-bg);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid var(--dock-border);
    border-radius: 24px;
    z-index: 1000;
    align-items: center;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
    transition: bottom var(--transition-normal);
}

/* On desktop/tablet the top nav already lists Projects/Experience/Blog/
   Contact, so keep the dock lightweight and hide the duplicate entries. */
.dock-item.nav-only {
    display: none;
}

.dock-item {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 48px;
    height: 48px;
    border-radius: 12px;
    color: var(--text-primary);
    text-decoration: none;
    font-size: 1.25rem;
    transition: transform var(--transition-fast), background-color var(--transition-fast);
}

.dock-item:hover {
    transform: scale(1.3) translateY(-10px);
    background-color: var(--accent-glow);
    color: #fff;
}

.dock-item.active {
    color: var(--accent-color);
    background-color: var(--accent-glow);
}

.dock-label {
    display: none;
}

/* Skip-to-content link: invisible until it receives keyboard focus, so a
   keyboard/screen-reader visitor can jump straight past the header/nav to
   #main-content instead of tabbing through every nav link and header
   control on every single page. Positioned off-screen rather than
   display:none/visibility:hidden so it still receives focus. */
.skip-link {
    position: absolute;
    top: -100px;
    left: 12px;
    z-index: 10000;
    background: var(--accent-color);
    color: #fff;
    padding: 10px 18px;
    border-radius: 8px;
    font-family: var(--font-primary);
    font-weight: 600;
    font-size: 0.9rem;
    text-decoration: none;
    transition: top var(--transition-fast);
}

.skip-link:focus {
    top: 12px;
}

/* Custom Interactive Cursor & Background */
.interactive-bg {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: -1;
    pointer-events: none;
    background: var(--bg-ambient, radial-gradient(circle at var(--mouse-x, 50%) var(--mouse-y, 50%), var(--accent-glow) 0%, transparent 60%));
}

.custom-cursor {
    width: 20px;
    height: 20px;
    border: 2px solid var(--accent-color);
    border-radius: 50%;
    position: fixed;
    top: 0;
    left: 0;
    /* Positioned purely via transform (translate3d, written from main.js) so
       movement is handled by the GPU compositor instead of triggering
       layout on every mousemove - see initInteractiveCursor(). */
    transform: translate3d(0, 0, 0) translate(-50%, -50%);
    will-change: transform;
    pointer-events: none;
    z-index: 9999;
    mix-blend-mode: difference;
    transition: width var(--transition-fast), height var(--transition-fast), background-color var(--transition-fast);
}

.custom-cursor.hovered {
    width: 40px;
    height: 40px;
    background-color: var(--accent-glow);
}

/* Accessibility settings support */
body.accessibility-contrast {
    --bg-primary: #ffffff !important;
    --bg-secondary: #f0f0f0 !important;
    --text-primary: #000000 !important;
    --text-secondary: #222222 !important;
    --accent-color: #0000ff !important;
}

body.accessibility-contrast[data-theme="dark"] {
    --bg-primary: #000000 !important;
    --bg-secondary: #111111 !important;
    --text-primary: #ffffff !important;
    --text-secondary: #eeeeee !important;
    --accent-color: #ffff00 !important;
}

body.reduced-motion * {
    transition-duration: 0s !important;
    animation-duration: 0s !important;
}

/* Typography styles */
h1, h2, h3, h4 {
    font-family: var(--font-primary);
    font-weight: 700;
}

p {
    line-height: 1.6;
    color: var(--text-secondary);
}

.badge {
    display: inline-block;
    padding: 4px 8px;
    border-radius: 8px;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    background-color: var(--accent-glow);
    color: var(--accent-color);
}

/* ==========================================================================
   Animated Theme Toggle (single pill switch, replaces separate sun/moon buttons)
   ========================================================================== */
.theme-switch {
    background: transparent;
    border: none;
    padding: 0;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.theme-switch-track {
    position: relative;
    display: block;
    width: 62px;
    height: 34px;
    border-radius: 999px;
    background: var(--bg-secondary);
    box-shadow: var(--nm-shadow-inset-dark);
    transition: background var(--transition-normal);
}

.theme-switch-thumb {
    position: absolute;
    top: 3px;
    left: 3px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--accent-color);
    box-shadow: 0 2px 8px var(--accent-glow);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform var(--transition-normal);
    transform: translateX(0);
}

[data-theme="dark"] .theme-switch-thumb {
    transform: translateX(28px);
}

.theme-switch-thumb i {
    position: absolute;
    font-size: 0.8rem;
    color: #fff;
    transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.theme-icon-sun {
    opacity: 1;
    transform: scale(1) rotate(0deg);
}

.theme-icon-moon {
    opacity: 0;
    transform: scale(0.5) rotate(-90deg);
}

[data-theme="dark"] .theme-icon-sun {
    opacity: 0;
    transform: scale(0.5) rotate(90deg);
}

[data-theme="dark"] .theme-icon-moon {
    opacity: 1;
    transform: scale(1) rotate(0deg);
}

/* Circular reveal animation driven by the View Transition API */
::view-transition-old(root),
::view-transition-new(root) {
    animation: none;
    mix-blend-mode: normal;
}

::view-transition-old(root) {
    z-index: 1;
}

::view-transition-new(root) {
    z-index: 9999;
}

/* Smooth crossfade when switching between UI styles (Neumorphism <->
   Glassmorphism <-> Neubrutalism etc). Scoped to .ui-style-transition (set
   briefly by setUiStyle() in main.js) so it doesn't fight the circular
   reveal animation used for the separate light/dark toggle above - both
   reuse the same ::view-transition-old/new(root) pseudo-elements, so the
   default `animation: none` only gets overridden while this class is
   present. */
html.ui-style-transition::view-transition-old(root) {
    animation: ui-style-fade-out 260ms ease both;
}

html.ui-style-transition::view-transition-new(root) {
    animation: ui-style-fade-in 260ms ease both;
}

@keyframes ui-style-fade-out {
    to {
        opacity: 0;
    }
}

@keyframes ui-style-fade-in {
    from {
        opacity: 0;
    }
}

/* Language switch button reuses nm-button styling; just ensure label doesn't wrap */
#lang-switch-label {
    pointer-events: none;
}

/* ==========================================================================
   Site Header — structural layout only; colors come from theme files.
   ========================================================================== */
.site-header {
    position: sticky;
    top: 0;
    z-index: 900;
    padding: 10px 16px;
    background: var(--dock-bg);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-bottom: 1px solid var(--dock-border);
    transition: top var(--transition-normal);
    /* margin-top: 40px !important; */
}

.site-header.translate-active {
    margin-top: 40px !important;
    top: 40px;
}

/* backdrop-filter (like filter/transform) makes an element a containing
   block for fixed-position descendants. .style-picker-menu switches to
   position:fixed on mobile (see the 640px query below) so it can anchor to
   the viewport instead of a cramped header row - but while nested inside
   .site-header, that only works if the header itself isn't also creating a
   containing block at the same time. Drop the header's backdrop-filter for
   the moment the menu is open, so "fixed" means the viewport as intended. */
.site-header.picker-open {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

@media (max-width: 768px) {
    .goog-te-menu-frame {
        left: 8px !important;
        right: 8px !important;
    }
}

.site-header-inner {
    display: flex;
    justify-content: space-between;
    align-items: center;
    max-width: 1200px;
    margin: 0 auto;
    flex-wrap: wrap;
    gap: 10px;
}

.brand {
    text-decoration: none;
    font-size: clamp(1.05rem, 4vw, 1.5rem);
    font-weight: 800;
    color: var(--accent-color);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 52vw;
}

.header-controls {
    display: flex;
    gap: 10px;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.icon-btn {
    width: 38px;
    height: 38px;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-shrink: 0;
}

.lang-btn {
    width: 44px;
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.5px;
}

.auth-btn {
    padding: 8px 16px;
    text-decoration: none;
    display: flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}

/* ==========================================================================
   Primary Navigation (Home / Projects / Experience / Blog / Contact)
   ========================================================================== */
.primary-nav-link {
    text-decoration: none;
    color: var(--text-secondary);
    font-weight: 600;
    font-size: 0.85rem;
    padding: 8px 14px;
    border-radius: 10px;
    transition: all var(--transition-fast);
}

.primary-nav-link:hover {
    color: var(--accent-color);
    background: var(--accent-glow);
}

.primary-nav-link.active {
    color: var(--accent-color);
    background: var(--accent-glow);
}

/* ---- Tablet: nav still visible, but wraps onto its own centered row ---- */
@media (max-width: 900px) {
    .primary-nav {
        order: 3;
        width: 100%;
        justify-content: center;
        background: var(--bg-secondary);
        border-radius: 14px;
        padding: 6px;
        gap: 2px;
    }

    .primary-nav-link {
        font-size: 0.8rem;
        padding: 7px 10px;
    }
}

/* ---- Mobile: top nav is replaced by the bottom dock navbar, but the
   header itself (brand, style picker, theme toggle, login/admin) stays
   visible and compact - it's the only place those controls live, since
   the dock only handles page navigation. ---- */
@media (max-width: 768px) {
    .primary-nav {
        display: none;
    }

    .brand {
        max-width: 42vw;
        font-size: 1.1rem;
    }

    .header-controls {
        gap: 6px;
    }

    .icon-btn {
        width: 34px;
        height: 34px;
        font-size: 0.9rem;
    }

    .lang-btn {
        width: 38px;
    }

    .theme-switch-track {
        width: 52px;
        height: 30px;
    }

    .theme-switch-thumb {
        width: 24px;
        height: 24px;
    }

    [data-theme="dark"] .theme-switch-thumb {
        transform: translateX(22px);
    }

    /* Auth button collapses to an icon-only circle to make room */
    .auth-btn {
        padding: 0;
        width: 38px;
        height: 38px;
        justify-content: center;
        border-radius: 50%;
    }

    .auth-btn span {
        display: none;
    }

    /* The dock now IS the primary navigation: show every section, spread
       evenly as a true bottom navbar, with labels under each icon. Items
       are fixed squares (not flex:1 stretched rectangles) with a real gap
       between them, so the active/hover highlight box never reads as one
       full-width bar - and both the square and the gap scale down together
       as the viewport narrows instead of the icons just getting cramped. */
    .floating-dock {
        display: flex;
        bottom: 0;
        left: 0;
        right: 0;
        top: auto;
        width: 100%;
        transform: translateZ(0);
        -webkit-transform: translateZ(0);
        justify-content: space-around;
        align-items: center;
        gap: clamp(2px, 1.5vw, 10px);
        padding: 6px 4px calc(6px + env(safe-area-inset-bottom, 0px));
        border-radius: 0;
        border: none;
        border-top: 1px solid var(--dock-border);
    }

    .dock-item.nav-only {
        display: flex;
    }

    .dock-item {
        flex: 0 1 auto;
        flex-direction: column;
        width: clamp(38px, 11.5vw, 56px);
        height: clamp(38px, 11.5vw, 56px);
        min-height: 0;
        gap: 2px;
        padding: 4px 2px;
        border-radius: 12px;
        font-size: clamp(0.9rem, 3.4vw, 1.05rem);
    }

    .dock-item:hover {
        transform: none;
    }

    .dock-label {
        display: block;
        font-size: clamp(0.5rem, 2vw, 0.62rem);
        font-weight: 600;
        line-height: 1;
        white-space: nowrap;
    }

    /* Keep page content clear of the fixed bottom navbar */
    #main-content {
        padding-bottom: 84px;
    }
}

/* ---- Very small phones: trim spacing a little further ---- */
@media (max-width: 380px) {
    .brand {
        max-width: 36vw;
        font-size: 1rem;
    }

    .header-controls {
        gap: 4px;
    }

    /* .dock-item / .dock-label sizing already scales continuously via
       clamp() in the 768px block above - no fixed override needed here. */
}

/* ==========================================================================
   GitHub Contribution Graph — theme-aware cells (no inline colors, so a
   theme toggle re-colors the whole graph instantly with zero JS re-render)
   ========================================================================== */
.contrib-cell {
    width: 100%;
    height: 100%;
    border-radius: 2px;
    background-color: var(--contrib-level-0);
    transition: background-color var(--transition-fast);
}
.contrib-cell.level-1 { background-color: var(--contrib-level-1); }
.contrib-cell.level-2 { background-color: var(--contrib-level-2); }
.contrib-cell.level-3 { background-color: var(--contrib-level-3); }
.contrib-cell.level-4 { background-color: var(--contrib-level-4); }

/* Contribution graph must always fully fit its card — no x/y scrollbars.
   minmax(0, 1fr) is the key fix: plain "1fr" resolves to minmax(auto, 1fr),
   whose implicit "auto" minimum lets grid tracks refuse to shrink below
   their content size and overflow the card; minmax(0, 1fr) removes that
   floor so all week columns always compress to fit the available width. */
#git-contributions {
    grid-auto-flow: column;
}
#git-contributions,
#contrib-months {
    max-width: 100%;
}

/* Lightweight shimmer used while GitHub stats/contribution data is loading,
   so the widget feels instant even before the network response lands */
.stat-skeleton {
    display: inline-block;
    min-width: 28px;
    min-height: 1.1em;
    border-radius: 4px;
    background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent), var(--skeleton-shine);
    background-size: 200% 100%;
    animation: skeleton-shimmer 1.2s ease-in-out infinite;
}
.contrib-skeleton .contrib-cell {
    animation: skeleton-shimmer 1.2s ease-in-out infinite;
    background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent), var(--contrib-level-0);
    background-size: 200% 100%;
}
@keyframes skeleton-shimmer {
    0% { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/* Custom tooltip for contribution graph cells — replaces the plain native
   "title" tooltip (unstyled, slow to appear) with something that matches
   the rest of the UI and shows a readable date instead of raw ISO text. */
.contrib-tooltip {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 999;
    padding: 6px 10px;
    border-radius: 8px;
    background: var(--text-primary);
    color: var(--bg-primary);
    font-size: 0.72rem;
    font-weight: 600;
    white-space: nowrap;
    pointer-events: none;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
    opacity: 0;
    transform: translateY(4px);
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.contrib-tooltip.is-below {
    transform: translateY(-4px);
}
.contrib-tooltip.is-visible {
    opacity: 1;
    transform: translateY(0);
}

/* Skill icon (Icon Class field) — shared sizing regardless of which icon
   font it came from (FontAwesome, Remix Icon, Bootstrap Icons, Ionicons,
   Material Symbols, ...) so mixed sets still line up next to skill names. */
.skill-icon {
    font-size: 1em;
    color: var(--accent-color);
    line-height: 1;
}
span.material-symbols-outlined.skill-icon {
    font-size: 1.1em;
    vertical-align: middle;
}
iconify-icon.skill-icon {
    font-size: 1.1em;
    vertical-align: middle;
}

/* ==========================================================================
   STYLE PICKER WIDGET (header dropdown) — structural only.
   Swatch colors below are intentionally fixed/illustrative previews of each
   theme's real identity, not tied to the currently active theme.
   ========================================================================== */
.style-picker {
    position: relative;
}

/* Accessibility menu: visually the same popover mechanics as the UI-style
   picker above (position, panel look, mobile viewport-anchoring) but kept
   as fully separate classes on purpose - so opening/closing this menu
   never opens/closes the style picker, and vice versa (see toggleA11yMenu()
   / toggleStylePicker() in main.js, which only ever touch their own
   element). */
.a11y-picker {
    position: relative;
}

.a11y-menu {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    width: 240px;
    background: var(--bg-secondary);
    border: 1px solid var(--glass-border, transparent);
    border-radius: 16px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
    padding: 8px;
    display: none;
    flex-direction: column;
    gap: 4px;
    z-index: 1200;
}

.a11y-menu.open {
    display: flex;
}

@media (max-width: 640px) {
    .a11y-menu {
        position: fixed;
        top: 60px;
        bottom: auto;
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        width: min(280px, calc(100vw - 32px));
    }
}

.a11y-option {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 10px;
    border-radius: 10px;
    background: transparent;
    border: none;
    color: var(--text-primary);
    font-family: var(--font-primary);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    text-align: left;
    transition: background var(--transition-fast);
}

.a11y-option i {
    width: 16px;
    text-align: center;
}

.a11y-option:hover {
    background: var(--accent-glow);
}

.a11y-option.active {
    background: var(--accent-color);
    color: #fff;
}

.style-picker-menu {
    position: absolute;
    top: calc(100% + 10px);
    right: 0;
    width: 220px;
    background: var(--bg-secondary);
    border: 1px solid var(--glass-border, transparent);
    border-radius: 16px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
    padding: 8px;
    display: none;
    flex-direction: column;
    gap: 4px;
    z-index: 1200;
}

.style-picker-menu.open {
    display: flex;
}

/* On mobile the button can sit anywhere in a cramped header row, so an
   absolutely-positioned "right: 0" menu can end up overflowing off the
   left edge of the viewport and getting clipped. Anchor it to the
   viewport itself instead, centered above the bottom dock navbar. */
@media (max-width: 640px) {
    .style-picker-menu {
        position: fixed;
        top: 60px;
        bottom: auto;
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        width: min(280px, calc(100vw - 32px));
    }
}

.style-picker-option {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 10px;
    border-radius: 10px;
    background: transparent;
    border: none;
    color: var(--text-primary);
    font-family: var(--font-primary);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    text-align: left;
    transition: background var(--transition-fast);
}

.style-picker-option:hover {
    background: var(--accent-glow);
}

.style-picker-option.active {
    background: var(--accent-color);
    color: #fff;
}

.style-picker-swatch {
    width: 18px;
    height: 18px;
    border-radius: 6px;
    flex-shrink: 0;
    display: inline-block;
}

.style-picker-swatch.sw-neumorphism {
    background: linear-gradient(135deg, #22d3ee, #0891b2);
    box-shadow: 2px 2px 4px rgba(0,0,0,0.4), -2px -2px 4px rgba(255,255,255,0.15);
}
.style-picker-swatch.sw-glassmorphism {
    background: linear-gradient(135deg, #a855f7, #ec4899);
    border: 1px solid rgba(255,255,255,0.4);
    opacity: 0.85;
}
.style-picker-swatch.sw-neubrutalism {
    background: #fde047;
    border-radius: 2px;
    border: 2px solid currentColor;
    box-shadow: 2px 2px 0 currentColor;
}
.style-picker-swatch.sw-bento {
    background: linear-gradient(135deg, #fb7185, #f59e0b);
    border-radius: 8px;
}
.style-picker-swatch.sw-claymorphism {
    background: linear-gradient(135deg, #34d399, #c084fc);
    border-radius: 10px;
    box-shadow: 2px 2px 5px rgba(0,0,0,0.35), -2px -2px 5px rgba(255,255,255,0.15);
}
.style-picker-swatch.sw-soft-pastel {
    background: linear-gradient(135deg, #ff8fc0, #ffb56b, #a68bff);
    border-radius: 12px 6px 12px 6px / 6px 12px 6px 12px;
    box-shadow: 2px 2px 6px rgba(0,0,0,0.2), -2px -2px 6px rgba(255,255,255,0.25);
}
.style-picker-swatch.sw-modern-glow {
    background: linear-gradient(120deg, #7c5cff, #22d3ee, #ff5ca8);
    border-radius: 6px;
    box-shadow: 0 0 8px rgba(124,92,255,0.6);
}

/* ==========================================================================
   TRUE BENTO MEDIA MOSAIC — structural grid mechanics (colors from theme)
   ========================================================================== */
.media-bento-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    grid-auto-rows: 90px;
    grid-auto-flow: dense;
    gap: 10px;
}

.media-bento-item {
    position: relative;
    border-radius: 16px;
    overflow: hidden;
    cursor: pointer;
    grid-column: span 3;
    grid-row: span 3;
    background: var(--bg-primary);
}

.media-bento-item img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--transition-normal);
}

.media-bento-item:hover img {
    transform: scale(1.06);
}

.media-bento-item .media-caption {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    padding: 8px 10px;
    font-size: 0.7rem;
    font-weight: 600;
    color: #fff;
    background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
}

.media-bento-item.ratio-wide   { grid-column: span 4; grid-row: span 2; }
.media-bento-item.ratio-landscape { grid-column: span 2; grid-row: span 2; }
.media-bento-item.ratio-square { grid-column: span 2; grid-row: span 2; }
.media-bento-item.ratio-tall   { grid-column: span 2; grid-row: span 3; }

/* Media Showcase snapshot (index page only) - always exactly 2 items, so
   instead of reusing the full mosaic's fixed-height rows (which don't
   grow) this is a simple 2-up grid whose height flexes to fill the card,
   the same way .timeline-teaser does for its neighbor in the same bento
   row. `flex: 1` lets it grow to match the Experience Timeline card next
   to it; the ratio classes are ignored here since both tiles are always
   the same size. */
.media-snapshot-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-auto-rows: unset;
    flex: 1;
}

.media-snapshot-grid .media-bento-item,
.media-snapshot-grid .media-bento-item.ratio-wide,
.media-snapshot-grid .media-bento-item.ratio-landscape,
.media-snapshot-grid .media-bento-item.ratio-square,
.media-snapshot-grid .media-bento-item.ratio-tall {
    grid-column: auto;
    grid-row: auto;
    height: 240px;
    min-height: 160px;
    position: relative;
}

.media-snapshot-grid .media-bento-item img,
.media-snapshot-grid .media-bento-item video {
    width: 100%;
    height: 100%;
    object-fit: cover; /* memenuhi seluruh area */
    display: block;
}

@media (max-width: 768px) {
    .media-snapshot-grid {
        grid-template-columns: 1fr;
    }

    .media-snapshot-grid .media-bento-item,
    .media-snapshot-grid .media-bento-item.ratio-wide,
    .media-snapshot-grid .media-bento-item.ratio-landscape,
    .media-snapshot-grid .media-bento-item.ratio-square,
    .media-snapshot-grid .media-bento-item.ratio-tall {
        height: 285px;
        width: 100%;
        min-height: 220px;
    }
}

/* Tablet: its own 3-column tier rather than inheriting the desktop 6-track
   mosaic at a squeezed width. Track math is simplified to whole-column
   spans (1, 2 or 3) so tiles land in clean, evenly aligned rows of 3. */
@media (min-width: 769px) and (max-width: 1099px) {
    .media-bento-grid:not(.media-snapshot-grid) {
        grid-template-columns: repeat(3, 1fr);
        grid-auto-rows: 110px;
    }

    .media-bento-grid:not(.media-snapshot-grid) .media-bento-item,
    .media-bento-grid:not(.media-snapshot-grid) .media-bento-item.ratio-landscape,
    .media-bento-grid:not(.media-snapshot-grid) .media-bento-item.ratio-square {
        grid-column: span 1;
        grid-row: span 2;
    }

    .media-bento-grid:not(.media-snapshot-grid) .media-bento-item.ratio-wide {
        grid-column: span 2;
        grid-row: span 2;
    }

    .media-bento-grid:not(.media-snapshot-grid) .media-bento-item.ratio-tall {
        grid-column: span 1;
        grid-row: span 3;
    }
}

@media (max-width: 768px) {
    .media-bento-grid {
        grid-template-columns: 1fr;
        grid-auto-rows: auto;
        gap: 14px;
    }
    /* Every tile goes full-width on mobile regardless of its desktop
       aspect-ratio span class - explicit rather than relying on the
       browser clamping an oversized span down to the available track
       count, which is the same visual result but isn't guaranteed. */
    .media-bento-item,
    .media-bento-item.ratio-wide,
    .media-bento-item.ratio-landscape,
    .media-bento-item.ratio-square,
    .media-bento-item.ratio-tall {
        grid-column: 1 / -1;
        grid-row: span 1;
        aspect-ratio: 4 / 3;
        height: auto;
    }
}

/* Video tiles thumbnail themselves (browser shows the video's own first
   frame via the #t=0.1 src fragment) -- styled exactly like an <img> tile. */
.media-bento-item video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    pointer-events: none;
    transition: transform var(--transition-normal);
}

.media-bento-item:hover video {
    transform: scale(1.06);
}

/* Small badge marking non-plain-image media (video / 360 / before-after) */
.media-type-icon {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 30px;
    height: 30px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.75rem;
    z-index: 2;
}

/* ==========================================================================
   GALLERY LIGHTBOX — full-size viewer opened by clicking any media tile.
   Images show full size, videos play with controls, 360 shots get a label,
   Before/After pairs render as a drag-to-compare slider.
   ========================================================================== */
.gallery-lightbox {
    position: fixed;
    inset: 0;
    z-index: 3000;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 30px 20px;
    background: rgba(0, 0, 0, 0.85);
    backdrop-filter: blur(6px);
}

.gallery-lightbox.open {
    display: flex;
    animation: modalFadeIn 0.2s ease;
}

.gallery-lightbox-panel {
    position: relative;
    width: 100%;
    max-width: 900px;
    max-height: 90vh;
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.gallery-lightbox-body {
    width: 100%;
    max-height: 68vh;
    border-radius: 16px;
    overflow: hidden;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.gallery-lightbox-body img,
.gallery-lightbox-body video {
    width: 100%;
    max-height: 70vh;
    object-fit: contain;
    display: block;
}

.gallery-lightbox-info {
    color: #fff;
    text-align: center;
}

.gallery-lightbox-title {
    margin: 4px 0;
    font-size: 1.1rem;
    font-weight: 700;
}

.gallery-lightbox-desc {
    margin: 0;
    font-size: 0.85rem;
    color: rgba(255, 255, 255, 0.7);
}

.gallery-lightbox-badge {
    display: none;
    padding: 4px 12px;
    border-radius: 999px;
    background: var(--accent-glow);
    color: var(--accent-color);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.02em;
}

.gallery-lightbox-close,
.gallery-lightbox-nav {
    position: absolute;
    border: none;
    cursor: pointer;
    color: #fff;
    background: rgba(255, 255, 255, 0.1);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all var(--transition-fast);
}

.gallery-lightbox-close {
    top: -46px;
    right: 0;
    width: 36px;
    height: 36px;
    border-radius: 10px;
}

.gallery-lightbox-close:hover {
    background: rgba(255, 255, 255, 0.25);
}

.gallery-lightbox-nav {
    top: 50%;
    transform: translateY(-50%);
    width: 44px;
    height: 44px;
    border-radius: 50%;
    font-size: 1rem;
}

.gallery-lightbox-prev { left: -10px; }
.gallery-lightbox-next { right: -10px; }

.gallery-lightbox-nav:hover {
    background: var(--accent-color);
}

@media (max-width: 640px) {
    .gallery-lightbox-prev { left: 4px; }
    .gallery-lightbox-next { right: 4px; }
}

/* Before / After comparison slider */
.ba-slider {
    position: relative;
    width: 100%;
    max-height: 68vh;
    aspect-ratio: 16 / 10;
    user-select: none;
}

.ba-slider .ba-after,
.ba-slider .ba-before {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.ba-slider .ba-before-wrap {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    clip-path: inset(0 50% 0 0);
}

.ba-slider .ba-before-wrap .ba-before {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.ba-handle {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 50%;
    width: 3px;
    background: #fff;
    box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
    transform: translateX(-50%);
    pointer-events: none;
}

.ba-handle::after {
    content: "\f337";
    font-family: "Font Awesome 6 Free";
    font-weight: 900;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: #fff;
    color: #111;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.8rem;
}

.ba-range {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: ew-resize;
    -webkit-appearance: none;
}

.ba-label {
    position: absolute;
    bottom: 12px;
    padding: 4px 10px;
    border-radius: 6px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

.ba-label-before { left: 12px; }
.ba-label-after { right: 12px; }

/* ==========================================================================
   GALLERY PAGE — category filter chips on the full /gallery listing.
   ========================================================================== */
.gallery-filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.gallery-filter-chip {
    padding: 8px 18px;
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--text-secondary);
    background: var(--bg-primary);
    box-shadow: var(--nm-shadow-inset-dark);
    text-decoration: none;
    transition: all var(--transition-fast);
    white-space: nowrap;
}

.gallery-filter-chip:hover {
    color: var(--accent-color);
}

.gallery-filter-chip.active {
    background: var(--accent-color);
    color: #fff;
    box-shadow: 0 4px 14px var(--accent-glow);
}

/* ==========================================================================
   Google Translate widget.
   Google's native widget is masked/hidden on EVERY breakpoint (desktop,
   tablet, mobile alike) and replaced visually with a small fixed flag-emoji
   pill ("🇬🇧"/"🇮🇩") on top of it. The pill is purely visual
   (pointer-events: none) - every click still reaches the real (invisible)
   widget underneath, so it's still 100% Google's own dropdown/menu doing
   the work, just never shown at its native (and messy, unstyled) size on
   any screen size.
   ========================================================================== */
.lang-widget{
    position:relative;
    width:44px;
    height:38px;
    border-radius:12px;
    overflow:hidden;
    cursor:pointer;
    background: var(--bg-secondary, #eef2f7);
    box-shadow: var(--nm-shadow-flat-dark, none), var(--nm-shadow-flat-light, none);
}

#google_translate_element {
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
    /* Deliberately not clipped itself: Google's widget renders at its own
       natural (and larger) size on some viewports, so overflow:hidden here
       would cut off part of the clickable area. It's invisible either way,
       so letting it overflow doesn't change anything visually - it just
       keeps the whole thing clickable. */
}

/* Hide Google's own "Powered by Google Translate" text/link as an extra
   safety net (the opacity:0 above already hides everything visually). */
#google_translate_element .goog-te-gadget {
    font-size: 0;
}
#google_translate_element .goog-te-gadget > span,
#google_translate_element .goog-te-gadget > a {
    display: none !important;
}

.lang-widget-label{
    display:flex;
    position:absolute;
    inset:0;
    align-items:center;
    justify-content:center;
    font-size:20px;
    font-family:inherit;
    pointer-events:none;
    z-index:2;
}

/* The banner iframe Google injects at the very top of the page when a
   translation is active - not used since we render the SIMPLE inline
   dropdown instead, and Google still reserves space for it otherwise. */
.goog-te-banner-frame {
    display: none !important;
}
body {
    top: 0 !important;
}

@media (max-width: 768px) {
    .lang-widget {
        width: 38px;
    }

    .lang-widget-label {
        font-size: 0.95rem;
        font-weight: 700;
    }
}

.goog-text-highlight {
    background: none !important;
    box-shadow: none !important;
}

/* ==========================================================================
   AI Assistant floating widget — responsive across desktop/tablet/mobile.
   Kept clear of the fixed bottom dock navbar on small screens.
   ========================================================================== */
.ai-widget {
    position: fixed;
    bottom: 80px;
    right: 20px;
    z-index: 9999;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 15px;
}

.ai-chat-window {
    width: 350px;
    max-width: calc(100vw - 40px);
    height: 500px;
    max-height: 70vh;
    flex-direction: column;
    padding: 15px;
    gap: 10px;
}

.ai-message-body p {
    margin: 0 0 8px 0;
    line-height: 1.5;
}

.ai-message-body p:last-child {
    margin-bottom: 0;
}

.ai-message-body ul,
.ai-message-body ol {
    margin: 4px 0 8px 0;
    padding-left: 20px;
    line-height: 1.5;
}

.ai-message-body li {
    margin-bottom: 4px;
}

.ai-message-body li:last-child {
    margin-bottom: 0;
}

.ai-message-body ul:last-child,
.ai-message-body ol:last-child {
    margin-bottom: 0;
}

.ai-message-body strong {
    font-weight: 700;
    color: var(--text-primary);
}

.ai-message-body em {
    font-style: italic;
}

.ai-message-body code {
    background: var(--nm-bg, rgba(128, 128, 128, 0.15));
    padding: 1px 5px;
    border-radius: 4px;
    font-size: 0.85em;
    font-family: monospace;
}

.ai-message-body h3,
.ai-message-body h4,
.ai-message-body h5,
.ai-message-body h6 {
    margin: 8px 0 4px 0;
    font-size: 0.95em;
}

.ai-message-body a {
    color: var(--accent-color);
    text-decoration: underline;
}

.ai-toggle-btn {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.5rem;
    flex-shrink: 0;
    color: var(--accent-color);
    box-shadow: var(--nm-shadow-flat-dark), var(--nm-shadow-flat-light);
}

@media (max-width: 768px) {
    .ai-widget {
        right: 12px;
        bottom: 92px; /* clears the fixed bottom dock navbar + safe area */
    }

    .ai-chat-window {
        width: calc(100vw - 24px);
        max-height: 65vh;
    }

    .ai-toggle-btn {
        width: 50px;
        height: 50px;
        font-size: 1.3rem;
    }
}

@media (max-width: 380px) {
    .ai-widget {
        right: 8px;
        bottom: 88px;
    }

    .ai-chat-window {
        width: calc(100vw - 16px);
    }
}

/* ==========================================================================
   GitHub Contribution Graph — layout (1 continuous row vs 3 stacked rows)
   is decided in JS from the card's measured width, not a viewport media
   query, since it depends on how wide the card itself renders (bento
   column span, theme padding, mobile dock, etc).
   ========================================================================== */
.contrib-container {
    min-height: 100px;
}

.contrib-half + .contrib-half {
    margin-top: 10px;
}

/* Width/margin-left are kept in sync with .contrib-weekday-labels' width
   + its own margin-right + .contrib-half-body's gap (20px + 4px + 3px =
   27px) so the month row lines up exactly above its grid's first column.
   The .is-narrow variant below uses smaller numbers for its own smaller
   weekday-label column. */
.contrib-months-row {
    display: grid;
    grid-auto-rows: 14px;
    margin-left: 27px;
    position: relative;
    height: 14px;
    width: calc(100% - 27px);
    overflow: hidden;
}

.contrib-months-row span {
    grid-row: 1;
    font-size: 0.62rem;
    color: var(--text-muted);
    line-height: 14px;
}

.contrib-half-body {
    display: flex;
    gap: 3px;
    width: 100%;
    min-width: 0;
}

/* 20px gives "Wed"/"Mon"/"Fri" enough room at this font size to never
   spill past its own column -- and margin-right adds an explicit gutter
   (like Tailwind's mx-1) on top of that, so the weekday labels never
   visually touch the contribution grid next to them, however narrow the
   card gets. */
.contrib-weekday-labels {
    display: grid;
    grid-template-rows: repeat(7, 11px);
    gap: 3px;
    width: 20px;
    margin-right: 4px;
    flex-shrink: 0;
}

.contrib-weekday-labels span {
    font-size: 0.6rem;
    color: var(--text-muted);
    overflow: hidden;
    white-space: nowrap;
}

.contrib-grid {
    display: grid;
    grid-template-rows: repeat(7, 11px);
    gap: 3px;
    flex-grow: 1;
    min-width: 0;
    overflow: hidden;
}

/* Narrow layout (mobile / a bento card collapsed to one column): the
   graph is shrunk further on top of the wide->3-row split, so weekday
   and month labels always have visibly more breathing room next to a
   smaller grid instead of fighting it for space. */
.contrib-container.is-narrow .contrib-half + .contrib-half {
    margin-top: 8px;
}

.contrib-container.is-narrow .contrib-months-row {
    grid-auto-rows: 13px;
    height: 13px;
    margin-left: 23px;
    width: calc(100% - 23px);
}

.contrib-container.is-narrow .contrib-months-row span {
    font-size: 0.58rem;
    line-height: 13px;
}

.contrib-container.is-narrow .contrib-half-body {
    gap: 2px;
}

.contrib-container.is-narrow .contrib-weekday-labels {
    width: 18px;
    margin-right: 3px;
    gap: 2px;
    grid-template-rows: repeat(7, 9px);
}

.contrib-container.is-narrow .contrib-weekday-labels span {
    font-size: 0.56rem;
}

.contrib-container.is-narrow .contrib-grid {
    gap: 2px;
    grid-template-rows: repeat(7, 9px);
}
/* ==========================================================================
   PAGINATION — used on Projects, Gallery and Blog landing pages.
   Layout/behavior lives here (theme-agnostic); colors come from whichever
   theme stylesheet is active via .nm-button and the --accent-color /
   --text-secondary variables, so it never needs per-theme overrides.
   ========================================================================== */
.pg-hidden {
    display: none !important;
}

/* ==========================================================================
   CARD GRID — shared by Projects and Blog (and anything else that's a flat
   list of equal-size cards, as opposed to the variable-size Gallery mosaic
   below). Fixed column counts per breakpoint rather than auto-fit, so the
   count is predictable instead of drifting to 5-6 columns on very wide
   screens: 1 column on mobile, 3 on tablet, 4 on desktop.
   ========================================================================== */
.card-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 20px;
}

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

@media (min-width: 1100px) {
    .card-grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

.pagination-bar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 6px;
}

.pagination-btn {
    min-width: 38px;
    height: 38px;
    padding: 0 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.85rem;
    font-family: inherit;
    cursor: pointer;
    transition: transform var(--transition-fast, 0.15s ease), opacity 0.15s ease;
}

.pagination-btn.pagination-page.active {
    background: var(--accent-color) !important;
    background-image: none !important;
    color: #fff !important;
    box-shadow: none !important;
    border-color: var(--accent-color) !important;
    cursor: default;
    transform: none;
}

.pagination-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
    pointer-events: none;
    transform: none;
}

.pagination-ellipsis {
    padding: 0 4px;
    color: var(--text-secondary);
    font-size: 0.85rem;
    user-select: none;
}

@media (max-width: 480px) {
    .pagination-btn {
        min-width: 34px;
        height: 34px;
        padding: 0 8px;
        font-size: 0.8rem;
    }
}
/* ---------------------------------------------------------------------
   Tech-stack / tag badges (projects.tech_stack & projects/blogs.tags).
   Admins can put plain text ("Netlify") or an image badge URL (e.g. a
   shields.io link) in the same field, so each entry renders as either a
   small text chip or a size-capped <img> - capped separately for
   desktop, tablet and mobile so a pasted badge never blows up the layout.
   ---------------------------------------------------------------------- */
.tag-badge-text {
    display: inline-block;
    font-size: 0.65rem;
    background-color: rgba(255, 255, 255, 0.05);
    color: var(--text-secondary);
    padding: 2px 6px;
    border-radius: 4px;
    line-height: 1.6;
}

.tag-badge-text-accent {
    background-color: var(--accent-glow);
    color: var(--accent-color);
    font-weight: 600;
}

.tag-badge-img {
    display: inline-block;
    height: 20px;
    max-width: 150px;
    width: auto;
    object-fit: contain;
    vertical-align: middle;
    border-radius: 3px;
}

/* Tablet */
@media (max-width: 992px) {
    .tag-badge-img {
        height: 18px;
        max-width: 130px;
    }
}

/* Mobile */
@media (max-width: 576px) {
    .tag-badge-img {
        height: 16px;
        max-width: 110px;
    }
}

/* =========================================================================
   Tech Stack (skills.show_proficiency = false)
   Simpler icon+name-only presentation of Skills, used instead of the
   "Core Expertise" %-bar layout whenever the admin toggle is off. Two
   different animated treatments on purpose: a compact staggered-pop grid
   for the small homepage bento card, and a continuous logo-marquee for
   the full Experience page list (which can have far more items).
   ========================================================================= */

/* ---- Home / bento card: compact grid, staggered pop-in + a gentle
   floating icon, so a handful of items still feel alive in a small card. */
.tech-stack-grid--compact {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 10px;
}

.tech-stack-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 10px;
    border-radius: 12px;
    font-size: 0.8rem;
    font-weight: 600;
    overflow: hidden;
    opacity: 0;
    transform: translateY(10px) scale(0.96);
    animation: tech-chip-pop-in 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}

.tech-stack-grid--compact .tech-stack-chip:nth-child(1) { animation-delay: 0.02s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(2) { animation-delay: 0.08s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(3) { animation-delay: 0.14s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(4) { animation-delay: 0.20s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(5) { animation-delay: 0.26s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(6) { animation-delay: 0.32s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(7) { animation-delay: 0.38s; }
.tech-stack-grid--compact .tech-stack-chip:nth-child(8) { animation-delay: 0.44s; }

.tech-stack-chip:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px var(--accent-glow);
}

.tech-stack-chip-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 26px;
    height: 26px;
    font-size: 1rem;
    color: var(--accent-color);
    animation: tech-chip-icon-float 2.6s ease-in-out infinite;
}

.tech-stack-chip-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@keyframes tech-chip-pop-in {
    to {
        opacity: 1;
        transform: translateY(0) scale(1);
    }
}

@keyframes tech-chip-icon-float {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-3px); }
}

/* ---- Experience page: full-width continuous marquee, two rows worth of
   chips duplicated back-to-back for a seamless infinite scroll. Pauses on
   hover/focus so names stay readable. */
.tech-stack-marquee-wrap {
    overflow: hidden;
    position: relative;
    width: 100%;
    max-width: 100%;
    /* `contain: layout paint` gives this element a strict, independent
       containing block: the max-content track inside (all chips laid out
       in one un-wrapping row, duplicated x2 - easily thousands of px wide)
       is guaranteed to never affect this box's own size or the page's
       scrollable width, on any browser. Without it, some mobile WebKit
       builds have let a max-content flex track like this leak into the
       document's horizontal scroll range despite the overflow:hidden here,
       making the whole page draggable sideways and shoving right-anchored
       fixed elements (the AI button) off past the visible edge. */
    contain: layout paint;
    padding: 4px 0;
    -webkit-mask-image: linear-gradient(90deg, transparent, black 6%, black 94%, transparent);
    mask-image: linear-gradient(90deg, transparent, black 6%, black 94%, transparent);
}

.tech-stack-marquee {
    display: flex;
    align-items: center;
    gap: 14px;
    width: max-content;
    /* Duration scales with the number of chips (--tech-chip-count, set
       inline per page from {{len .skills}}) so the scroll SPEED stays
       constant no matter how many skills are added. Previously this was a
       flat 32s regardless of chip count: with ~10 skills that's a relaxed
       pace, but with ~25 the same track (now 2.5x longer, since it's
       duplicated end-to-end for the seamless loop) had to cover a much
       longer distance in the same 32s - a much faster, jerkier scroll,
       and more chips compositing/repainting per second on top of that is
       what made it feel rough, especially on mobile. clamp() keeps very
       small or very large lists from going too fast or unreasonably slow.
       3.2s/chip matches the original 32s pace at the seeded ~10 skills. */
    --tech-marquee-duration: clamp(18s, calc(var(--tech-chip-count, 10) * 3.2s), 100s);
    animation: tech-marquee-scroll var(--tech-marquee-duration) linear infinite;
    /* Its own compositor layer: with many chips duplicated x2 for the
       loop, letting the browser recompute layout/paint for the whole
       track on every frame (instead of just moving an already-painted
       layer) is what causes visible stutter on mobile once the list gets
       long. */
    will-change: transform;
    transform: translateZ(0);
    backface-visibility: hidden;
}

.tech-stack-marquee-wrap:hover .tech-stack-marquee,
.tech-stack-marquee-wrap:focus-within .tech-stack-marquee {
    animation-play-state: paused;
}

@keyframes tech-marquee-scroll {
    from { transform: translateX(0) translateZ(0); }
    to { transform: translateX(-50%) translateZ(0); }
}

.tech-stack-chip-lg {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 18px;
    border-radius: 999px;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
    flex-shrink: 0;
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.tech-stack-chip-lg:hover {
    transform: translateY(-3px) scale(1.04);
    box-shadow: 0 8px 20px var(--accent-glow);
}

.tech-stack-chip-lg-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.15rem;
    color: var(--accent-color);
}

@media (prefers-reduced-motion: reduce) {
    .tech-stack-chip {
        opacity: 1;
        transform: none;
        animation: none;
    }
    .tech-stack-chip-icon {
        animation: none;
    }
    .tech-stack-marquee {
        animation: none;
    }
}

/* =========================================================================
   Trix rich-text output (blog content, project descriptions, bio,
   experience/education description). Shared between the live Trix editor
   in admin (trix-editor.admin-trix) and however that saved HTML gets
   rendered on the public site, since both load this stylesheet.
   ========================================================================= */

/* Text alignment: admin-trix.js registers alignLeft/Center/Right/Justify
   as Trix block attributes rendered as these custom tags. They're not
   registered Web Components, just plain elements the browser lays out
   according to this CSS. */
align-left,
align-center,
align-right,
align-justify {
    display: block;
}
align-left { text-align: left; }
align-center { text-align: center; }
align-right { text-align: right; }
align-justify { text-align: justify; }

/* Lists: browsers give <ul>/<ol> a default indent of ~40px (and disagree on
   whether it's margin or padding), which starts the marker/number well to
   the right of where a plain paragraph's text begins. Right next to normal
   body text, that reads as the list "leaning in" instead of lining up with
   it. Resetting to one small, consistent indent keeps the marker close to
   the paragraph's own left edge, in both the live Trix editor and however
   the saved HTML renders on the public site. */
trix-editor ul,
trix-editor ol,
.trix-content ul,
.trix-content ol,
ul,
ol {
    margin: 0.4em 0;
    padding-left: 1.25em;
}

trix-editor li,
.trix-content li,
li {
    margin-bottom: 0.3em;
}

trix-editor li:last-child,
.trix-content li:last-child,
li:last-child {
    margin-bottom: 0;
}

/* Blockquote: distinctly different from plain text - italic, a large
   decorative quotation mark, and a left accent bar - instead of the
   near-invisible default browser blockquote. */
trix-editor blockquote,
.trix-content blockquote,
blockquote {
    position: relative;
    margin: 14px 0;
    padding: 6px 20px 6px 40px;
    font-style: italic;
    color: var(--text-secondary);
    border-left: 3px solid var(--accent-color);
}

trix-editor blockquote::before,
.trix-content blockquote::before,
blockquote::before {
    content: "\201C";
    position: absolute;
    left: 8px;
    top: -2px;
    font-family: Georgia, "Times New Roman", serif;
    font-style: normal;
    font-size: 2.2rem;
    line-height: 1;
    color: var(--accent-color);
    opacity: 0.6;
}

/* =========================================================================
   SCROLL-REVEAL ANIMATIONS
   Every page (Home/bento sections, Projects, Experience, Gallery, Blog,
   Contact) gets its own on-scroll entrance animation, each visually
   distinct on purpose (slide up/down/left/right, zoom, flip, pop, plain
   fade) so no two sections feel copy-pasted.

   Performance approach:
   - Only `opacity` + `transform` are animated - both are compositor-only
     properties, so the browser never re-runs layout or paint for these,
     regardless of how many elements are mid-animation at once.
   - main.js's initScrollReveal() uses a single shared IntersectionObserver
     (one native layout listener for the whole page, not a scroll handler)
     and unobserves each element the instant it has revealed once - so the
     cost is paid once per element, never again for the rest of the visit.
   - Nothing is hidden - and this whole block is inert - unless JS actually
     attaches the observer (see the html.reveal-enabled gate below). No-JS
     visitors, and anyone whose browser lacks IntersectionObserver, always
     get the fully visible page with zero animation cost.
   - will-change is only ever applied to the pending (not-yet-revealed)
     state and is dropped the moment an element reveals, so the compositor
     doesn't keep hinting/allocating layers for elements that will never
     move again.
   - prefers-reduced-motion (OS-level) and the site's own manual "reduced
     motion" accessibility toggle (body.reduced-motion) both fully disable
     this system - see the two overrides at the bottom.
   ========================================================================= */
html.reveal-enabled [data-reveal] {
    opacity: 0;
    transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1), transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
    will-change: opacity, transform;
}

html.reveal-enabled [data-reveal].is-revealed {
    opacity: 1;
    transform: none !important;
    will-change: auto;
}

/* Distinct per-section entrance directions/effects */
html.reveal-enabled [data-reveal="up"]        { transform: translateY(32px); }
html.reveal-enabled [data-reveal="down"]      { transform: translateY(-28px); }
html.reveal-enabled [data-reveal="left"]      { transform: translateX(-36px); }
html.reveal-enabled [data-reveal="right"]     { transform: translateX(36px); }
html.reveal-enabled [data-reveal="zoom"]      { transform: scale(0.92); }
html.reveal-enabled [data-reveal="zoom-fade"] { transform: scale(0.94) translateY(16px); }
html.reveal-enabled [data-reveal="fade"]      { transform: none; }
html.reveal-enabled [data-reveal="flip"]      { transform: perspective(900px) rotateX(14deg) translateY(18px); }

/* "pop" gets its own bouncier easing on the way in (only while revealing -
   the hidden/pending state above still uses the shared easing) */
html.reveal-enabled [data-reveal="pop"]  { transform: scale(0.8); }
html.reveal-enabled [data-reveal="pop"].is-revealed {
    transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* Cascading stagger for repeated items (bento sections, project/blog/gallery
   cards, cert & achievement grids). Add class="reveal-stagger" to the
   *parent* wrapping a run of [data-reveal] children. Capped at 8 explicit
   steps - anything beyond that shares the last delay so a 40-item grid
   never makes visitors wait seconds for the last row. */
.reveal-stagger > [data-reveal]:nth-child(1) { transition-delay: 0.03s; }
.reveal-stagger > [data-reveal]:nth-child(2) { transition-delay: 0.09s; }
.reveal-stagger > [data-reveal]:nth-child(3) { transition-delay: 0.15s; }
.reveal-stagger > [data-reveal]:nth-child(4) { transition-delay: 0.21s; }
.reveal-stagger > [data-reveal]:nth-child(5) { transition-delay: 0.27s; }
.reveal-stagger > [data-reveal]:nth-child(6) { transition-delay: 0.33s; }
.reveal-stagger > [data-reveal]:nth-child(7) { transition-delay: 0.39s; }
.reveal-stagger > [data-reveal]:nth-child(8) { transition-delay: 0.45s; }
.reveal-stagger > [data-reveal]:nth-child(n+9) { transition-delay: 0.5s; }

/* Experience timeline: alternating left/right slide-in per entry, layered
   on top of the base [data-reveal] system above (entries carry
   data-reveal="left" for the observer to pick up; this class only swaps
   the direction on even rows for the classic zig-zag timeline feel). */
html.reveal-enabled .timeline-entry:nth-child(even) {
    transform: translateX(36px);
}

@media (prefers-reduced-motion: reduce) {
    html.reveal-enabled [data-reveal] {
        opacity: 1 !important;
        transform: none !important;
        transition: none !important;
        will-change: auto;
    }
}

body.reduced-motion [data-reveal] {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
    will-change: auto;
}