/* ==========================================================================
   devnest.sk — skins and scenes
   --------------------------------------------------------------------------
   Part 1 re-declares colour tokens per app. Nothing else: the header, the
   spacing, the components and the type all come from site.css, so a visitor
   moving between two apps sees the same furniture in a different colour.

   Part 2 holds the drawn scenes — the hero illustrations and the app cards.
   They are hand-built HTML and SVG rather than images so they answer to the
   tokens above and stay sharp at any size.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. App skins

   Each app declares its light values on [data-app], and its dark values on
   :root[data-appearance="dark"] [data-app]. tools/chrome.py copies the dark
   values into the region after them, for a visitor whose system is dark.

   The selector is [data-app] and not body[data-app], so anything can wear an
   app's colours: the <body> of the app's own pages, and the app's card on
   the home page.

   The colours are the app's own: Tracktiv's from Assets.xcassets,
   AssetScout's from the two fills in AppIcon.icon.
   -------------------------------------------------------------------------- */

/* Tracktiv — the blue of the app icon, the green of its ring ------------- */

[data-app="tracktiv"] {
    --accent:      #2f6fcc;
    --accent-2:    #6aa4e8;
    --accent-ink:  #ffffff;
    --accent-soft: rgba(47, 111, 204, .11);
    --accent-line: rgba(47, 111, 204, .36);
    --ring:        #4da368;

    /* The category colours Tracktiv paints a day with */
    --c-activity:  #82c9be;
    --c-work:      #7297cb;
    --c-devnest:   #3d74c2;
    --c-meeting:   #a37bcc;
    --c-gaming:    #d69566;
    --c-fun:       #89c167;
    --c-browsing:  #77c589;
}
:root[data-appearance="dark"] [data-app="tracktiv"] {
    --accent:      #4fa3ff;
    --accent-2:    #8cc6ff;
    --accent-ink:  #04121f;
    --accent-soft: rgba(79, 163, 255, .16);
    --accent-line: rgba(79, 163, 255, .42);
    --ring:        #7cc98c;
}
/* chrome:dark-system */
@media (prefers-color-scheme: dark) {
    :root:not([data-appearance="light"]) [data-app="tracktiv"] {
        --accent:      #4fa3ff;
        --accent-2:    #8cc6ff;
        --accent-ink:  #04121f;
        --accent-soft: rgba(79, 163, 255, .16);
        --accent-line: rgba(79, 163, 255, .42);
        --ring:        #7cc98c;
    }
}
/* /chrome:dark-system */

/* AssetScout — the amber of the icon, over the charcoal of its label ------

   The icon's own amber, #e8a24a, is too light to read as text on white: it
   sits at about 2.1:1 against it, and the site uses --accent for links. The
   light value is therefore the same hue darkened to 4.9:1, and the icon
   colour itself becomes --accent-2. On a dark page the amber is bright
   enough on its own, so there it is the accent. */

[data-app="assetscout"] {
    --accent:      #a9631b;
    --accent-2:    #e8a24a;
    --accent-ink:  #ffffff;
    --accent-soft: rgba(169, 99, 27, .10);
    --accent-line: rgba(169, 99, 27, .34);
    --label:       #2a2f33;
}
:root[data-appearance="dark"] [data-app="assetscout"] {
    --accent:      #e8a24a;
    --accent-2:    #f4c68d;
    --accent-ink:  #231703;
    --accent-soft: rgba(232, 162, 74, .16);
    --accent-line: rgba(232, 162, 74, .42);
    --label:       #f1eadd;
}
/* chrome:dark-system */
@media (prefers-color-scheme: dark) {
    :root:not([data-appearance="light"]) [data-app="assetscout"] {
        --accent:      #e8a24a;
        --accent-2:    #f4c68d;
        --accent-ink:  #231703;
        --accent-soft: rgba(232, 162, 74, .16);
        --accent-line: rgba(232, 162, 74, .42);
        --label:       #f1eadd;
    }
}
/* /chrome:dark-system */

/* ==========================================================================
   2. Scenes
   ========================================================================== */

/* --------------------------------------------------------------------------
   2.1 The home stage: three Mac windows, stacked and drifting

   The drift is two things at once. A slow idle float runs on its own, and a
   pointer parallax is written into --px / --py by site.js. Both move the whole
   window. They stay in separate CSS properties — the float writes `translate`,
   the parallax writes `transform` — so neither has to know about the other.
   Moving only the content inside the frame would look wrong: the window border
   would stand still while its title bar slides up and down.
   -------------------------------------------------------------------------- */

.stage {
    position: relative;
    max-width: 940px;
    margin-inline: auto;
    aspect-ratio: 16 / 10;
    perspective: 1400px;
    text-align: left;   /* the hero centres its text; the windows must not */
    container: stage / inline-size;
}

.stage__win {
    position: absolute;
    border-radius: 12px;
    overflow: hidden;
    background: var(--surface);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-lg);
    transform: translate3d(calc(var(--px, 0) * var(--depth, 1) * 1px),
                           calc(var(--py, 0) * var(--depth, 1) * 1px), 0);
    transition: transform .45s var(--ease);
}
.stage__win { animation: float 9s ease-in-out infinite; }

.stage__win--back  { left: 0;    top: 2%;  width: 58%; --depth: .5; animation-delay: -1.5s; }
.stage__win--mid   { right: 0;   top: 10%; width: 52%; --depth: 1.1; animation-delay: -4.5s; }
.stage__win--front { left: 12%;  bottom: 0; width: 62%; --depth: 1.7; z-index: 2; }

/* The positions above are set for a phone or a tablet. The stage grows with the
   page, but the text in the windows does not, so on a wide stage every window
   is shorter compared with the stage. With the same positions, the timer then
   covered 155px of the code window, and a 24px gap opened between the timer
   and the pictures (measured on the full 940px stage).

   On a wide stage two things change, and the timer keeps its place:
   - The code window is narrower. Its right part was empty, and now the timer
     only touches its corner instead of covering it.
   - The picture grid is taller (see .thumbs), so the picture window reaches
     up to the code window and covers about 20px of the timer's bottom edge.
     That is the empty strip under "Today". The timer must not sit lower: the
     two windows float in opposite phase, 9px each way, and a lower timer
     would let the picture window cover the "Today" line. */
@container stage (min-width: 900px) {
    .stage__win--back { width: 50%; }
}

@keyframes float {
    0%, 100% { translate: 0 0; }
    50%      { translate: 0 -9px; }
}

.winbar {
    display: flex;
    align-items: center;
    gap: .4rem;
    padding: .5rem .7rem;
    background: var(--surface-2);
    border-bottom: 1px solid var(--line);
}
.winbar i { width: 10px; height: 10px; border-radius: 50%; }
.winbar i:nth-child(1) { background: #ff5f57; }
.winbar i:nth-child(2) { background: #febc2e; }
.winbar i:nth-child(3) { background: #28c840; }
.winbar span {
    margin-left: .5rem;
    font-family: var(--font-mono);
    font-size: .68rem;
    letter-spacing: .04em;
    color: var(--muted);
}

/* The windows are containers, so a window can change its own layout when the
   stage gets narrow. The page width alone does not say how wide a window is. */
.stage__win { container: win / inline-size; }

/* The timer window: a clock that runs, today's blocks, and the week as bars.

   The clock counts in CSS alone. --tick-m and --tick-s are registered as
   integers, so an animation steps them 0, 1, 2 … instead of jumping from the
   first value to the last one halfway. A counter then prints each one with a
   leading zero. The negative delays start the clock at 01:42:18, not at
   01:00:00: -18s for the seconds, and -2538s (42 × 60 + 18) for the
   minutes, so both roll over at the same moment. With reduced motion the
   animation ends at once, and the clock shows the declared values: the same
   01:42:18, standing still. */
@property --tick-m { syntax: "<integer>"; inherits: false; initial-value: 0; }
@property --tick-s { syntax: "<integer>"; inherits: false; initial-value: 0; }

.timer {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    gap: 1.2rem;
    align-items: start;
    padding: .9rem 1rem 1rem;
}
.timer__task {
    display: flex;
    align-items: center;
    gap: .45rem;
    margin: 0;
    font-size: .74rem;
    color: var(--ink-2);
}
.timer__task i {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--good);
    animation: blink 2s ease-in-out infinite;
}
.timer__clock {
    --tick-m: 42;
    --tick-s: 18;
    counter-reset: m var(--tick-m) s var(--tick-s);
    margin: .2rem 0 .75rem;
    font-family: var(--font-mono);
    font-size: 1.55rem;
    font-weight: 500;
    letter-spacing: -.02em;
    line-height: 1.2;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    animation: tick-s 60s steps(60, end) -18s infinite,
               tick-m 3600s steps(60, end) -2538s infinite;
}
.timer__clock::after { content: "01:" counter(m, decimal-leading-zero) ":" counter(s, decimal-leading-zero); }
@keyframes tick-s { from { --tick-s: 0; } to { --tick-s: 60; } }
@keyframes tick-m { from { --tick-m: 0; } to { --tick-m: 60; } }
@keyframes blink  { 50% { opacity: .35; } }

/* Today as a strip of blocks. The empty part at the end is the rest of the day. */
.timer__day {
    display: flex;
    gap: 2px;
    height: 7px;
    border-radius: 4px;
    overflow: hidden;
    background: var(--surface-2);
    box-shadow: inset 0 0 0 1px var(--line);
}
.timer__day i { width: var(--w); background: var(--accent); }
.timer__day i:nth-child(2n) { background: var(--accent-2); }
.timer__day i:nth-child(4)  { background: var(--line-strong); }
.timer__sum {
    display: flex;
    justify-content: space-between;
    gap: .5rem;
    margin: .5rem 0 0;
    font-size: .68rem;
    color: var(--muted);
}
.timer__sum b { font-family: var(--font-mono); font-weight: 500; color: var(--ink-2); }
.timer__week .timer__sum { margin: 0 0 .35rem; }

/* In a narrow window the week does not fit. Beside the clock it is squeezed,
   and the front window covers it. Under the clock it makes the window taller
   than the stage. So it is left out. The 420px limit was chosen by looking at
   the stage: at a 768px wide page the timer window is about 360px wide, and the
   week was already hidden. */
@container win (max-width: 420px) {
    .timer { grid-template-columns: minmax(0, 1fr); }
    .timer__week { display: none; }
}

/* The thumbnail window: a grid of pictures under a month heading, scrolled a
   little down and back up again, the way someone looks for one picture.

   The heading stays in place and the grid slides under it. The pictures are
   drawn with gradients, so no image has to load for the hero. They keep the
   same colours in the dark appearance, the same way real photographs do. */
.thumbs {
    position: relative;
    aspect-ratio: 16 / 8;
    overflow: hidden;
}
/* On a wide stage the grid is taller. See the note under the window positions. */
@container stage (min-width: 900px) {
    .thumbs { aspect-ratio: 16 / 9; }
}
.thumbs__head {
    position: absolute;
    inset: 0 0 auto;
    z-index: 1;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 0;
    padding: .45rem .8rem;
    font-size: .7rem;
    color: var(--ink-2);
    background: color-mix(in srgb, var(--surface) 76%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-bottom: 1px solid var(--line);
}
.thumbs__head span { display: flex; align-items: center; gap: .35rem; }
.thumbs__head span + span { color: var(--muted); font-variant-numeric: tabular-nums; }
.thumbs__head svg {
    width: 10px; height: 10px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.thumbs__grid {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: .5rem;
    padding: 2.35rem .6rem .6rem;
    animation: browse 12s infinite;
}
.thumbs__grid i {
    display: block;
    padding: 3px 3px 0;
    border-radius: 6px;
    background: var(--surface-2);
    box-shadow: inset 0 0 0 1px var(--line);
}
.thumbs__grid i::before {
    content: "";
    display: block;
    aspect-ratio: 16 / 10;
    border-radius: 3px;
    background: var(--photo, var(--line));
}
/* The caption under each picture, where the app prints the date */
.thumbs__grid i::after {
    content: "";
    display: block;
    width: 62%;
    height: 4px;
    margin: 7px auto;
    border-radius: 2px;
    background: var(--line-strong);
}
.thumbs__grid .is-selected { background: var(--accent); box-shadow: none; }
.thumbs__grid .is-selected::after { background: color-mix(in srgb, var(--accent-ink) 70%, transparent); }

/* Ten pictures, repeated. With four columns and ten pictures, the same picture
   never lands straight under itself in the next rows. */
.thumbs__grid i:nth-child(10n+1)  { --photo: radial-gradient(circle at 72% 44%, #ffe2b0 5%, transparent 6%), linear-gradient(#f2b27d, #d98a8a 52%, #3f5a82 52%, #2b3f5f); }
.thumbs__grid i:nth-child(10n+2)  { --photo: radial-gradient(120% 90% at 32% 112%, #4a5a4c 50%, transparent 51%), radial-gradient(90% 70% at 85% 110%, #6b7d69 50%, transparent 51%), linear-gradient(#b8cbe0, #e3ebf2); }
.thumbs__grid i:nth-child(10n+3)  { --photo: radial-gradient(150% 70% at 70% 112%, #c9864f 50%, transparent 51%), linear-gradient(#9fc3e2, #e2c7a5); }
.thumbs__grid i:nth-child(10n+4)  { --photo: radial-gradient(40% 30% at 30% 20%, rgba(255, 255, 255, .35), transparent), linear-gradient(#5fa8c9, #1f5f86 60%, #173f5c); }
.thumbs__grid i:nth-child(10n+5)  { --photo: linear-gradient(to top, #2a2d3a 28%, transparent 28%), radial-gradient(circle at 60% 66%, #ffd28a 7%, transparent 8%), linear-gradient(#6d7fa6, #e9a877 70%); }
.thumbs__grid i:nth-child(10n+6)  { --photo: radial-gradient(90% 60% at 22% 104%, #d5dee7 60%, transparent 61%), radial-gradient(90% 70% at 88% 108%, #8f9fae 55%, transparent 56%), linear-gradient(#eef3f7, #c5d3df); }
.thumbs__grid i:nth-child(10n+7)  { --photo: radial-gradient(70% 40% at 50% 55%, #4f7a4c 50%, transparent 51%), linear-gradient(#a9c7a0, #6d946a 55%, #3e5f6f 55%, #2c4756); }
.thumbs__grid i:nth-child(10n+8)  { --photo: radial-gradient(160% 95% at 50% 135%, #2f6fb5 55%, #8fc2f0 57.5%, #0b1020 60%); }
.thumbs__grid i:nth-child(10n+9)  { --photo: radial-gradient(22% 12% at 38% 66%, #34384a 60%, transparent 62%), linear-gradient(#e7a7b5, #c9a7d6 58%, #7a8bb5 58%, #4f5f8a); }
.thumbs__grid i:nth-child(10n+10) { --photo: radial-gradient(80% 30% at 40% 70%, #f4f6f8 50%, transparent 70%), linear-gradient(#9bb3c9, #e2e8ec 62%, #c9d3db); }

/* One scroll takes 1.8 s and eases out, like a flick on a trackpad. The
   distance is a share of the grid's own height: the grid has five rows, so
   22% is a little more than one row. */
@keyframes browse {
    0%, 16%  { transform: translateY(0);    animation-timing-function: cubic-bezier(.3, .6, .2, 1); }
    31%, 58% { transform: translateY(-22%); animation-timing-function: cubic-bezier(.3, .6, .2, 1); }
    73%, 100% { transform: translateY(0); }
}

/* The code window types itself out, one line at a time */
.code {
    padding: .9rem 1rem;
    font-family: var(--font-mono);
    font-size: .72rem;
    line-height: 1.75;
    color: var(--muted);
    overflow: hidden;
}
/* The lines keep their own `pre`, but the container must not: with `pre` on the
   wrapper the newlines between the spans become blank lines of their own. */
.code span {
    display: block;
    overflow: hidden;
    white-space: pre;
    width: 0;
    animation: type 9s steps(40, end) infinite;
    animation-delay: calc(var(--i, 0) * .35s);
}
.code b { color: var(--accent); font-weight: 500; }
@keyframes type {
    0%        { width: 0; }
    12%, 78%  { width: 100%; }
    92%, 100% { width: 0; }
}

/* The week as one bar per day, Monday to Sunday. The numbers agree with the
   text around them. Today is Friday, and its bar is the "Today" value, 6h 12m.
   The five bars add up to the week total: 6h 40m + 7h 05m + 5h 50m + 5h 37m +
   6h 12m = 31h 24m. A full bar is 8 hours, a chosen scale, so 6h 40m is 83%.
   Saturday and Sunday have not happened yet, so they show only the empty
   track. The bars grow in one after another when the page opens.

   After that, every 8 seconds a small wave runs across the bars from Monday
   to Friday. Each bar dips a little, springs up past its value, and settles
   back. The wave writes `scale`, and the growth writes `transform`, so the
   two animations never fight over one property. The tallest bar is 89%, and
   the wave stretches it by 7% at most, to about 95%, so no bar leaves its
   track. */
.bars {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: .45rem;
    height: 5.6rem;
}
.bars span {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: .3rem;
    font-family: var(--font-mono);
    font-size: .6rem;
    color: var(--muted);
}
.bars i {
    position: relative;
    flex: 1;
    width: 100%;
    border-radius: 4px;
    background: var(--surface-2);
}
.bars i::before {
    content: "";
    position: absolute;
    inset: auto 0 0;
    height: var(--h, 0%);
    border-radius: inherit;
    background: color-mix(in srgb, var(--accent) 38%, var(--surface));
    transform-origin: bottom;
    /* The -4s delay moves the first wave forward, so it comes 2.4s after the
       page opens instead of 6.4s. By then every bar has finished growing. */
    animation: grow 1s var(--ease) both,
               nudge 8s ease-in-out infinite;
    animation-delay: calc(.4s + var(--i, 0) * .09s),
                     calc(var(--i, 0) * .1s - 4s);
}
.bars .is-today { color: var(--ink); font-weight: 500; }
.bars .is-today i::before { background: var(--accent); }
@keyframes grow { from { transform: scaleY(0); } }
/* The bar rests for the first 80% of the cycle. The wave takes the last 1.6s. */
@keyframes nudge {
    0%, 80%, 100% { scale: 1 1; }
    84%           { scale: 1 .93; }
    91%           { scale: 1 1.07; }
    96%           { scale: 1 .985; }
}

/* --------------------------------------------------------------------------
   2.2 App cards on the home page
   -------------------------------------------------------------------------- */

.appgrid {
    display: grid;
    gap: 1.1rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 1fr));
}

.appcard {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: .9rem;
    padding: 1.6rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    text-decoration: none;
    overflow: hidden;
    isolation: isolate;
    transition: transform .35s var(--ease), border-color .25s var(--ease), box-shadow .35s var(--ease);
}
/* Each card carries its app's data-app, so it wears the app's skin from part
   1 and --accent is the app's own colour. The row of cards then reads as
   several products rather than several copies of one. */

.appcard:hover {
    transform: translateY(-4px);
    border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
    box-shadow: var(--shadow-md);
}

/* The hover tint is a full-width band behind the header row — not a radial
   blob — so it reads as a coloured header that fades into the card body,
   rather than a patch floating behind the icon. It bleeds to the card's own
   edges (top/left/right match .appcard's padding). The stops are fixed
   lengths, not percentages: the solid zone (0–4.5rem) is sized to clear the
   1.6rem bleed plus the header row itself, so the icon, title and status
   line all sit inside the solid colour instead of in the fade. The fade
   then runs another 4.5rem, deliberately bleeding into the body text. */
.appcard__top { position: relative; display: flex; align-items: center; gap: .8rem; }
.appcard__top::before {
    content: "";
    position: absolute;
    inset: -1.6rem -1.6rem -4.6rem -1.6rem;
    z-index: -1;
    background: linear-gradient(to bottom,
        color-mix(in srgb, var(--accent) 20%, var(--surface)) 0,
        color-mix(in srgb, var(--accent) 20%, var(--surface)) 4.5rem,
        transparent 9rem);
    opacity: 0;
    transition: opacity .35s var(--ease);
}
.appcard:hover .appcard__top::before { opacity: 1; }
.appcard__icon {
    display: block;
    flex: none;
    width: 46px; height: 46px;
    border-radius: 11px;
    box-shadow: var(--shadow-sm);
    transition: transform .35s var(--ease);
}
.appcard:hover .appcard__icon { transform: scale(1.06) rotate(-3deg); }
.appcard__name { font-size: 1.16rem; font-weight: 600; letter-spacing: -.02em; }
.appcard__state {
    display: flex;
    align-items: center;
    gap: .4rem;
    font-family: var(--font-mono);
    font-size: .68rem;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}
.appcard__body { font-size: .95rem; line-height: 1.55; color: var(--ink-2); flex: 1; }
.appcard__go {
    display: inline-flex;
    align-items: center;
    gap: .4rem;
    font-size: .9rem;
    font-weight: 500;
    color: var(--accent);
}
.appcard__go svg { width: 15px; height: 15px; stroke-width: 2.2; transition: transform .25s var(--ease); }
.appcard:hover .appcard__go svg { transform: translateX(3px); }

/* --------------------------------------------------------------------------
   2.3 Tracktiv scenes
   -------------------------------------------------------------------------- */

/* A ring that fills to the value in --p (0…1) once it is scrolled into view */
.ring { display: block; width: 100%; height: auto; }
.ring__track { fill: none; stroke: var(--line-strong); stroke-width: 9; }
.ring__value {
    fill: none;
    stroke: var(--ring, var(--accent));
    stroke-width: 9;
    stroke-linecap: round;
    stroke-dasharray: var(--circ, 314);
    stroke-dashoffset: var(--circ, 314);
    transform: rotate(-90deg);
    transform-origin: 50% 50%;
    transition: stroke-dashoffset 1.6s var(--ease);
}
.ring.is-in .ring__value,
html:not(.js) .ring__value { stroke-dashoffset: calc(var(--circ, 314) * (1 - var(--p, .6))); }

/* The day strip is a real screenshot, wiped in from the left as if the day
   were being written while you watch. */
.wipe { clip-path: inset(0 100% 0 0); transition: clip-path 1.8s var(--ease); }
.wipe.is-in { clip-path: inset(0 0 0 0); }

/* A row of category chips, in the app's own colours */
.chips { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: 1.4rem; }
.chip {
    display: inline-flex;
    align-items: center;
    gap: .45rem;
    padding: .3rem .7rem .3rem .55rem;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--surface);
    font-size: .84rem;
    color: var(--ink-2);
}
.chip i { width: 9px; height: 9px; border-radius: 3px; background: var(--c, var(--accent)); }

/* Big measured numbers */
.stats {
    display: grid;
    gap: 1.4rem 2.5rem;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 160px), 1fr));
    margin-top: 2.2rem;
}
.stat__n {
    font-size: clamp(1.9rem, 3.4vw, 2.6rem);
    font-weight: 600;
    letter-spacing: -.035em;
    font-variant-numeric: tabular-nums;
    color: var(--ink);
}
.stat__n small { font-size: .5em; font-weight: 500; color: var(--muted); margin-left: .15em; }
.stat__l { font-size: .86rem; color: var(--muted); }

/* --------------------------------------------------------------------------
   2.4 A plain "what it does / what is left" checklist, for an app that is
   not out yet
   -------------------------------------------------------------------------- */

.roadmap { display: grid; gap: .1rem; margin: 0; padding: 0; list-style: none; }
.roadmap li {
    display: grid;
    grid-template-columns: 22px minmax(0, 1fr);
    gap: .7rem;
    align-items: start;
    padding: .8rem 0;
    border-bottom: 1px solid var(--line);
    font-size: .97rem;
    color: var(--ink-2);
}
.roadmap li:last-child { border-bottom: 0; }
.roadmap svg { width: 18px; height: 18px; margin-top: .18em; stroke-width: 2.2; color: var(--accent); }
.roadmap b { display: block; color: var(--ink); font-weight: 600; }

/* --------------------------------------------------------------------------
   2.5 AssetScout scenes
   -------------------------------------------------------------------------- */

/* Three folders dropped on the empty window, over and over.

   The picture under the scene is a real capture, so nothing inside it can
   move. Everything that moves is drawn on top of it: the stack of folders,
   the pointer that holds them, and the two marks the landing leaves. The
   scene draws no app interface of its own, and it does not touch the arrow in
   the picture either — the folders land in the empty part of the window above
   it, where a hand would drop them, and the message under the arrow stays
   readable the whole time.

   The round is one move and one landing. The folders come in from off the
   page, leaning the way they are carried, straighten as they arrive, wait,
   and are then thrown off: they shrink away in a quarter of a second while a
   ring opens from the point where they were let go. There is no second,
   correcting move, because a hand does not make one.

   .dropscene__drag and .dropscene__land are layers as large as the picture,
   so the percentages in them are percentages of the picture, not of the
   folders. Both end at --drop-x / --drop-y, which is where the folders are
   let go. site.js writes those two again before each round, so the drop never
   lands in the same place twice; the values below are what a page without
   scripting gets.

   One round takes 5s: 1.4s of travel, 1s held, the release at 2.4s, the ring
   gone by 3.6s, and the rest is the pause before it starts again. */

/* The folders start off the right edge of the page, not at the edge of the
   picture, so they read as something brought in from outside rather than as
   part of the screenshot. Nothing may clip them before that, and the picture
   least of all, so the section takes the job. It is as wide as the page, so
   the scene can reach past the picture without pushing the page sideways.
   `clip` and not `hidden`, and only on one axis: the section must not become
   a scroll container, and the vertical side of it has nothing to cut. */
.section:has(.dropscene) { overflow-x: clip; }

.dropscene {
    position: relative;
    /* The picture is centred inside .shot--free at the same width, and the
       overlays are placed in percentages of it, so the scene has to stop
       where the picture stops. */
    max-width: var(--w, 100%);
    margin-inline: auto;
}

/* Everything hangs off the arrow in the picture, measured on the file:
   empty.webp is 1782x1408, and the arrow glyph covers x 808-973, y 546-745.
   So the arrow is at 50% across and 45.8% down. --drop-y then carries the
   folders up from there, far enough that they clear the arrow entirely. */
.dropscene__drag {
    position: absolute;
    inset: 0;
    pointer-events: none;
    /* Where the folders rest: with the animation skipped for reduced motion,
       this is the still the scene falls back to. */
    translate: var(--drop-x, 8%) var(--drop-y, -17%);
    animation: as-drag 5s cubic-bezier(.2, .68, .28, 1) infinite;
}

/* The stack runs two animations at once, because the two halves of what it
   does want different easings. as-stack is the plain fade, and as-lean is the
   tilt and the size — the lean it is carried at, and the snap away at the
   end. They never touch the same property, so neither has to know about the
   other. */
.dropscene__stack {
    position: absolute;
    left: 50%;
    top: 45.8%;
    width: 19%;
    height: auto;
    transform: translate(-50%, -50%);
    filter: drop-shadow(0 8px 14px rgba(9, 16, 26, .22));
    animation: as-stack 5s linear infinite,
               as-lean 5s cubic-bezier(.4, .08, .25, 1) infinite;
}
/* The pointer holds the stack above and left of its middle, which is where a
   hand grabs a folder. It has to stay clear of the badge in the corner, so
   these two numbers are not the stack's corner but a point inside it. */
.dropscene__pointer {
    position: absolute;
    left: 46.8%;
    top: 43%;
    width: 3.2%;
    height: auto;
    filter: drop-shadow(0 2px 4px rgba(9, 16, 26, .3));
    animation: as-pointer 5s linear infinite;
}

/* What the landing leaves behind: a ring that opens outwards and a glow under
   it. Both are the page's own flourish, not something the app draws, so they
   are gone in a second. The ring is what makes the drop land — a fade on its
   own reads as the folders simply going missing. */
.dropscene__land {
    position: absolute;
    inset: 0;
    pointer-events: none;
    translate: var(--drop-x, 8%) var(--drop-y, -17%);
}
/* The scale of these three, and of the stack above, is written into the
   `transform` next to the centring translate and not into the `scale`
   property. The two are not the same: `scale` is applied after `transform`,
   so it would shrink the -50% that centres the mark as well, and the mark
   would slide away from the point it is supposed to mark while it grows. */
.dropscene__glow,
.dropscene__ring {
    position: absolute;
    left: 50%;
    top: 45.8%;
    aspect-ratio: 1;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    opacity: 0;
}
.dropscene__glow {
    width: 15%;
    background: radial-gradient(circle,
        color-mix(in srgb, var(--accent) 50%, transparent), transparent 70%);
    animation: as-glow 5s var(--ease) infinite;
}
.dropscene__ring {
    width: 13%;
    border: 2px solid var(--accent);
    animation: as-ring 5s cubic-bezier(.15, .75, .3, 1) infinite;
}

/* The travel. The start is written as three quarters of a picture plus a
   fifth of the window, because a share of the picture alone is not enough: on
   a wide page the picture is small and the empty margin beside it is large,
   and the folders would appear out of nothing in that margin instead of
   coming in from the side. The 20vw is what carries them past the edge of the
   page at any width, from a 375px phone to a 1920px display.

   -36% puts the start at the height of the window's title bar, so they come
   in from the right and a little from above. From there it is one move: the
   animation's own ease slows them into the drop point and they stop. */
@keyframes as-drag {
    0%        { translate: calc(75% + 20vw) -36%; }
    28%, 100% { translate: var(--drop-x, 8%) var(--drop-y, -17%); }
}

/* The fade in is over while the folders are still off the page. */
@keyframes as-stack {
    0%        { opacity: 0; }
    6%, 48%   { opacity: 1; }
    57%, 100% { opacity: 0; }
}

/* Carried in at a tilt and a little larger. The tilt is given a slower curve
   than the travel and 34% instead of 28%, so it is still leaning while it
   crosses the window and only comes straight once it has settled. With the
   travel's own curve the lean was over before the folders were on the page at
   all.

   On release the easing turns around: the folders hold still for the first
   moments and then go, instead of slipping away from the start. */
@keyframes as-lean {
    0%        { transform: translate(-50%, -50%) scale(1.12) rotate(-12deg); }
    34%, 48%  { transform: translate(-50%, -50%) scale(1) rotate(0deg);
                animation-timing-function: cubic-bezier(.5, 0, .85, .35); }
    57%, 100% { transform: translate(-50%, -50%) scale(.55) rotate(4deg); }
}

/* The pointer stays a moment after the folders are gone, then fades. */
@keyframes as-pointer {
    0%        { opacity: 0; }
    6%, 53%   { opacity: 1; }
    64%, 100% { opacity: 0; }
}

@keyframes as-glow {
    0%, 48%   { opacity: 0; transform: translate(-50%, -50%) scale(.3); }
    54%       { opacity: .85; transform: translate(-50%, -50%) scale(.85); }
    68%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.25); }
}

/* The ring starts a touch before the folders have finished going, so the two
   read as one event rather than as a fade followed by a ring. */
@keyframes as-ring {
    0%, 48%   { opacity: 0; transform: translate(-50%, -50%) scale(.25); }
    51%       { opacity: .85; transform: translate(-50%, -50%) scale(.45); }
    72%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(2.1); }
}
