/* noteriks.photo — portfolio v5. Sections are appended in task order. */

@font-face{font-family:'Archivo';src:url('../fonts/Archivo-VF.woff2') format('woff2');font-weight:300 900;font-stretch:62% 125%;font-display:swap}

:root{
  --bg:oklch(0.938 0.003 250);--ink:oklch(0.17 0.005 250);--mute:oklch(0.47 0.006 250);
  --rule:oklch(0.85 0.004 250);--well:oklch(0.90 0.004 250);--accent:oklch(0.60 0.20 29);
  --on-accent:oklch(0.98 0.003 250);--viewer-bg:oklch(0.13 0.004 250);--viewer-ink:oklch(0.94 0.003 250);
  --vh:max(100svh,540px);
  --ph:max(180px,min(calc(var(--vh) - 284px),93.33vw));--top:max(28px,calc((var(--vh) - 234px - var(--ph)) / 2));--ease:cubic-bezier(.2,.7,.2,1);--tint-op:.07;
}
@media (prefers-color-scheme:dark){:root{
  --bg:oklch(0.155 0.004 250);--ink:oklch(0.93 0.003 250);--mute:oklch(0.66 0.006 250);
  --rule:oklch(0.29 0.005 250);--well:oklch(0.20 0.005 250);--accent:oklch(0.67 0.19 29);
}}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}

html,body{margin:0;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased;-webkit-tap-highlight-color:transparent}   /* every control has a pressed state of its own */
/* 17px lines put every text row and hairline on a whole pixel (12px × 1.4 = 16.8px did not) */
/* no text on the page can be selected (the owner's choice): a drag across it highlights nothing */
body{font-family:'Archivo',system-ui,sans-serif;font-size:12px;line-height:17px;letter-spacing:.005em;text-transform:lowercase;overflow:hidden;-webkit-user-select:none;user-select:none}
/* the app never gets shorter than 540px; a shorter window scrolls down to the footer instead of losing it */
@media (max-height:539.98px){body{overflow-y:auto}}
html.is-viewing{overflow:hidden;background:var(--viewer-bg)}   /* the page (scrollable only in short windows) holds still behind the viewer; where a phone shows the page's edge beside it (a notch, in landscape), that edge is the viewer's colour */
[hidden]{display:none!important}
button{all:unset;box-sizing:border-box;cursor:pointer;touch-action:manipulation}   /* two quick taps are two taps, not a zoom */
button:disabled{cursor:default}
button:focus-visible,[tabindex]:focus-visible{outline:1.5px solid var(--accent);outline-offset:3px}
.vw:focus-visible{outline:none}   /* the dialog itself holds the focus when it opens from a link: nothing to ring */
.mute{color:var(--mute)}
.dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);flex:none}

/* --- shell --- */
.app{position:relative;height:var(--vh);display:grid;grid-template-rows:52px minmax(0,1fr) 156px;grid-template-columns:minmax(0,1fr);overflow:hidden}
/* background tint: out of the grid flow from the start, coloured by main.js in task 11. --tint-op:0 switches it off */
.tint{position:absolute;inset:0;z-index:0;pointer-events:none;opacity:var(--tint-op);transition:background-color 1.6s ease,opacity 1s ease}
.hd{position:relative;z-index:1;min-width:0;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:16px;padding:0 24px;border-bottom:1px solid var(--rule)}
.hd__name{font-stretch:62%;font-weight:800;font-size:24px;line-height:1;letter-spacing:-.01em}
.hd__run{font-stretch:62%;font-weight:700;font-size:20px;line-height:1;letter-spacing:-.005em;word-spacing:.1em;white-space:nowrap}   /* the condensed face's own space is 2px here, and "september 2026" read as one word */
.hd__right{justify-self:end;display:flex;align-items:center;gap:20px}
.hd__count{color:var(--mute);white-space:nowrap;font-variant-numeric:tabular-nums}
.hd__views{display:flex;gap:14px}
.mn{position:relative;z-index:1;min-width:0;min-height:0;transition:opacity .2s ease}
.mn.is-fading{opacity:0;pointer-events:none}
.ft{position:relative;z-index:1;min-width:0;display:grid;grid-template-rows:38px 44px 20px 24px minmax(0,1fr);grid-template-columns:minmax(0,1fr);padding:0 24px;border-top:1px solid var(--ink)}
.ft__hint{display:flex;justify-content:space-between;align-items:center;gap:16px;color:var(--mute);font-size:11px;white-space:nowrap;overflow:hidden}
.ft__hint-l{overflow:hidden;text-overflow:ellipsis}
@media (max-width:639.98px){
  .ft__hint-r,.hint-long{display:none}
  /* no room for the count on a phone, but it is the live region that speaks a filter's result: out of sight, not gone */
  .hd__count{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
}
@media (min-width:640px){.hint-short{display:none}}

/* --- strip --- */
.strip{height:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;overscroll-behavior-x:contain}
.strip:focus-visible{outline-offset:-3px}
.strip::-webkit-scrollbar{display:none}
.strip__track{position:relative;height:100%;width:max-content;min-width:100%;box-sizing:border-box;display:flex;align-items:flex-start;gap:20px;padding:var(--top) 40px 0 24px}
.strip__track.is-few{justify-content:center}
.intro{flex:none;width:min(380px,74vw);height:var(--ph);display:flex;flex-direction:column;justify-content:space-between;gap:24px;padding-right:24px;box-sizing:border-box}
/* margin-top puts the tops of the tall letters on the intro's top edge, which is the photos' too (as the month type is) */
.intro__lede{margin:-.22em 0 0;font-size:clamp(17px,1.5vw,21px);line-height:1.28;letter-spacing:-.012em;max-width:18em;text-wrap:pretty}
.ph{flex:none;margin:0;display:flex;flex-direction:column;gap:10px;color:var(--ink);cursor:zoom-in;transition:color .25s}
.ph.is-focus{color:var(--accent)}   /* keyboard focus on the photo; set by strip.js */
.ph__box{display:block;height:var(--ph);max-width:140vw;overflow:hidden;cursor:zoom-in}
.ph__box picture{display:block;width:100%;height:100%}
.ph__box img{display:block;width:100%;height:100%;object-fit:contain}
.ph__box img,.gr__cell img{-webkit-user-drag:none}   /* a drag across the strip is not a drag of the photo (img.js sets draggable off too) */
.ph__cap{display:flex;justify-content:space-between;gap:10px;line-height:17px;font-variant-numeric:tabular-nums}
.ph__num{display:flex;align-items:center;gap:6px}
.ph__date{color:var(--mute)}
/* a photo is wider than a phone, so a caption spread to its two ends never showed both: below 640px the date follows the number */
@media (max-width:639.98px){.ph__cap{justify-content:flex-start}}

/* --- without JS: a plain sideways list --- */
.no-js .arrow,.no-js .ft > :not(.ft__hint){visibility:hidden}
/* With JS: the strip's track is out of sight until main.js has built the real one, so its panels and its opening
   position are first seen where they stay. By visibility, not display: the photos in it still load meanwhile.
   The inline script in the head takes the class off after 3s if main.js never does. The arrows wait too, as they have
   nothing to move yet. */
.is-booting .strip__track,.is-booting .arrow{visibility:hidden}

/* --- strip panels (task 4) --- */
.spec{margin:0;display:flex;flex-direction:column;border-top:1px solid var(--ink)}
.spec > div{display:flex;justify-content:space-between;gap:12px;padding:8px 0;border-bottom:1px solid var(--rule)}
.spec dt{color:var(--mute)}
.spec dd{margin:0;text-align:right;font-variant-numeric:tabular-nums}
.intro__lower{display:flex;flex-direction:column;gap:22px}
.intro__acts{display:flex;flex-direction:column;gap:6px}
/* the two red links that point somewhere: text and an arrow 10px apart (14px under the pointer), the body size */
.intro__go,.endp__back{display:flex;align-items:center;align-self:flex-start;gap:10px;color:var(--accent)}   /* as wide as their text, not the column */
.intro__new{display:flex;align-items:center;gap:6px}
/* The garden (garden.js): a plant grown from the photos on show, first in the intro's lower block, so it sits on the
   table. As wide as the column; its height is set in script from the blank there is. 10px to its caption, as a photo
   to its own. The caption is muted, its first line in ink; each line is its own block and never wraps (garden.js keeps
   them short), so no word is left alone on a line. Under 640px wide (garden.js marks the figure gdn--low) the plant
   stands on the table's rule and its caption is out of sight, not gone: a screen reader still reads what it is. */
.gdn{margin:0;display:flex;flex-direction:column;gap:10px;opacity:0;transition:opacity .3s ease}
.gdn.is-in{opacity:1}
.gdn canvas{display:block}
.gdn__cap{color:var(--mute);font-variant-numeric:tabular-nums}
.gdn__cap span{display:block;white-space:nowrap}
.gdn__head{color:var(--ink)}
.gdn--low .gdn__cap{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
/* short windows: the intro gives up air before it gives up a line */
@media (max-height:579.98px){.intro{gap:12px}.intro__lower{gap:16px}.intro .spec > div{padding:6px 0}}
.mp{flex:none;height:var(--ph);display:flex;flex-direction:column;justify-content:space-between;padding:0 16px 0 20px;clip-path:inset(-12px 0 0 0);border-left:1px solid var(--ink)}   /* clipped at the sides for the drift, open at the top: the hook of an f rises a hair above the ascender line */
/* margin-top puts the flat tops of the tall letters (l, d, b) on the photo's top edge; one value for every month, so all names share a baseline.
   min-width: every panel is as wide as the widest month name (may, 1.339em at this weight and width), so the first photo of
   every month starts at the same x. In em of the big type itself, so it holds at every window size. A test measures the names. */
.mp__big{display:flex;flex-direction:column;will-change:transform;font-stretch:62%;font-weight:800;line-height:.8;letter-spacing:-.03em;font-size:min(calc(var(--ph) * .3),250px);margin-top:-.007em;min-width:1.34em}
/* the stem of an m or n on the left edge of the count below it; on the lines, not the box, so name and count still drift as one box */
.mp__big > span{margin-left:-.036em}
/* The year line sits .13em below the name: the deepest descender (j, g, y: .111em below the .8 line) clears it by .02em.
   One value for every month, so the lockup is the same height whether or not the name has a descender. */
.mp__big > span + span{margin-top:.13em}
.mp__count{white-space:nowrap}
/* the month name and its count drift as one: strip.js sets --px on the panel */
.mp__big,.mp__count{transform:translateX(var(--px,0))}
.gp{flex:none;width:clamp(96px,10vw,140px);height:var(--ph);box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;border-left:1px dashed var(--mute);background:repeating-linear-gradient(135deg,transparent 0 7px,var(--rule) 7px 8px)}
/* one 20px inset from a panel's rule to its text, as in the month panels */
.gp__text{color:var(--mute);background:var(--bg);padding:4px 6px 0 20px;text-wrap:balance}
.endp{flex:none;width:240px;height:var(--ph);box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;gap:12px;padding-left:20px;border-left:1px solid var(--ink)}
.endp__label{font-size:15px;letter-spacing:-.01em}

/* image reveal: only images made by img.js start hidden, so static markup is never invisible */
img.rv{opacity:0;transform:scale(1.02);transition:opacity .7s ease,transform 1.2s var(--ease)}
img.rv.is-in{opacity:1;transform:none}

.arrow{position:absolute;top:calc(var(--top) + var(--ph) / 2);margin-top:-22px;width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;background:color-mix(in oklch,var(--bg) 78%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:0 0 0 1px var(--rule),0 6px 18px rgba(0,0,0,.08);transition:opacity .3s ease,box-shadow .25s}
.arrow--prev{left:12px;opacity:0;pointer-events:none}
.arrow--next{right:12px}
@media (hover:none) and (pointer:coarse){.arrow{display:none}}   /* a finger scrolls the strip itself */
@media (max-width:639.98px){.arrow{display:none}}   /* and below 640px, where the drift, the hints and the timeline floor change too, there is no room for them beside the month panel */

/* --- timeline (task 6) --- */
.tl{position:relative;height:26px;margin-top:18px;display:flex;align-items:stretch;cursor:ew-resize;touch-action:none;user-select:none;-webkit-user-select:none}
/* identical box model on all three rows: border-box, 1px left border, no horizontal padding */
.tl__seg,.tl-m,.tl-y{box-sizing:border-box;border-left:1px solid transparent;padding-left:0;padding-right:0;white-space:nowrap}
.tl__seg{height:100%;display:flex;align-items:flex-end;gap:1px}
.tl.is-tight .tl__seg{gap:0}
.tl__seg--gap{background:repeating-linear-gradient(135deg,transparent 0 3px,var(--rule) 3px 4px)}
.tl__s{flex:1 1 0;min-width:0;height:100%;transform-origin:bottom;pointer-events:none;transition:transform 220ms var(--ease),opacity .3s ease}
.tl__s.is-out{opacity:.14}
.tl__ind{position:absolute;top:-4px;bottom:-3px;left:0;width:0;min-width:3px;box-sizing:border-box;border:1.5px solid var(--accent);pointer-events:none;opacity:0;transition:left .28s var(--ease),width .28s var(--ease),top 220ms var(--ease),opacity .3s ease}
.tl__tag{position:absolute;bottom:100%;left:-1.5px;margin-bottom:2px;padding:0 4px;background:var(--accent);color:var(--on-accent);font-size:10px;line-height:12px;white-space:nowrap;transition:opacity .2s ease}
.tl__peek{position:absolute;bottom:calc(100% + 28px);left:0;width:180px;box-sizing:border-box;z-index:10;pointer-events:none;opacity:0;transition:opacity .16s ease,transform .12s ease-out;background:var(--bg);border:1px solid var(--ink);padding:5px;display:flex;flex-direction:column;gap:5px;box-shadow:0 12px 30px rgba(0,0,0,.14)}
.tl__peek-box{height:112px;background:var(--well);overflow:hidden}
.tl__peek-box img{display:block;width:100%;height:100%;object-fit:contain}
.tl__peek-row{display:flex;justify-content:space-between;gap:10px;white-space:nowrap;font-variant-numeric:tabular-nums}
/* states of the bar: is-hot (a bar is raised), is-peek (preview showing), is-drag (pointer down), has-ind (box showing) */
.tl.is-hot .tl__tag,.tl.is-peek .tl__tag{opacity:0}
.tl.is-drag .tl__ind{transition:top 220ms var(--ease),opacity .3s ease}
/* keyboard focus is drawn on the thumb (the red box) by thickening it: a ring round it would run into the tag above
   and the month ticks below. The tag keeps its place on the thicker edge. With no box (grid, nothing to show) the bar
   takes the ring every other control has, which fits between the bars and the month row. */
.tl:focus-visible{outline:none}
.tl:focus-visible .tl__ind{box-shadow:none;border-width:2.5px}
.tl:focus-visible .tl__tag{left:-2.5px;margin-bottom:3px}
.tl:focus-visible:not(.has-ind){outline:1.5px solid var(--accent);outline-offset:3px}
/* the gap preview is one line: no empty rows, centred */
.tl__peek span:empty{display:none}
.tl__peek.is-gap .tl__peek-row{justify-content:center}
.tl__peek.is-gap{bottom:calc(100% + 8px)}   /* nothing is raised under a gap, so it can sit close */

.tl-months{display:flex;min-width:0;padding-top:4px}
.tl-m{overflow:hidden;padding-top:2px;line-height:14px;text-indent:3px;border-left-color:var(--rule);transition:color .25s,background .25s}
.tl-m--gap{border-left-color:transparent}
.tl-m.is-year{border-left-color:var(--ink)}
.tl-m.is-dim{color:var(--mute)}
.tl-m:disabled{color:var(--rule)}
.tl-m.is-active{border-left-color:var(--accent);background:color-mix(in oklch,var(--accent) 12%,transparent);color:var(--accent);font-weight:600}
.tl-m:focus-visible{outline-offset:-1.5px}
.tl-note{color:var(--mute);padding-top:2px}

/* The year rule is drawn once, on the row: as a border on each cell it broke at every join, the cells being fractions
   of a pixel wide. A gap cell covers its stretch of the rule and dashes it. No years (colour sort), no rule. */
.tl-years{display:flex;min-width:0;padding-top:2px;background:linear-gradient(var(--ink),var(--ink)) 0 2px/100% 1px no-repeat}
.tl-years:empty{background:none}
.tl-y{height:18px;padding-top:4px;text-indent:3px;font-weight:600;line-height:14px}
.tl-y--gap{padding-top:3px;border-top:1px dashed var(--mute);background:linear-gradient(var(--bg),var(--bg)) border-box 0 0/100% 1px no-repeat}
.tl-y--tick{border-left-color:var(--ink)}

/* cell widths: months share the bar by photo count; below 640px the 18px floor is dropped so the whole archive fits */
.tl-c--month{min-width:18px}
.tl-c--all{min-width:0}
.tl-c--gap{flex:0 0 12px;min-width:12px}
@media (max-width:639.98px){
  .tl-c--month{min-width:0}
  /* a phone's first screen: 30% smaller month type and a 12px right padding, so the first photo shows about 200px of
     itself; the 20px inset from the rule to the name stays, as in the gap panel */
  .mp__big{font-size:calc(var(--ph) * .21)}
  .mp{padding-right:12px}
  .tl-c--gap{flex-basis:6px;min-width:6px}
}
/* up to 760px both filters and the sort do not fit on one line: the two group labels step aside (and the row can scroll) */
@media (max-width:759.98px){.ft__group > .mute{display:none}}

/* --- controls (task 7) --- */
.hd__view{padding:4px 0 3px;color:var(--mute);border-bottom:1.5px solid transparent;transition:color .25s,border-color .25s}
.hd__view[aria-pressed="true"]{color:var(--ink);border-bottom-color:var(--accent);cursor:default}   /* a click on it would change nothing */
/* The row runs edge to edge, its contents on the page margins. Where it has to scroll, a cut-off item then reads as
   "more this way" rather than as clipping; where it does not, the tap areas of its end items have room in the padding
   instead of making it scrollable. It never scrolls up and down. */
.ft__controls{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 -24px;padding:0 24px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
.ft__controls::-webkit-scrollbar{display:none}
.ft__group{display:flex;align-items:center;gap:10px;flex:none}
.ft__group--sort{gap:14px}
.sw-row{display:flex}
.sw{width:30px;height:30px;display:flex;align-items:center;justify-content:center;transition:opacity .3s}
/* the focus ring goes round the dot, outside the ring of the selected state: round like the control */
.sw:focus-visible{outline:none}
.sw:focus-visible .sw__dot{outline:1.5px solid var(--accent);outline-offset:5px}
.sw.is-dim{opacity:.4}
.sw:disabled{opacity:.22}
.sw__dot{width:16px;height:16px;border-radius:50%;transition:box-shadow .3s,transform .3s}
.sw[aria-pressed="true"] .sw__dot{box-shadow:0 0 0 2px var(--bg),0 0 0 3.5px var(--accent)}
.sw__dot--red{background:oklch(0.68 0.11 28)}
.sw__dot--orange{background:oklch(0.68 0.11 58)}
.sw__dot--yellow{background:oklch(0.68 0.11 90)}
.sw__dot--green{background:oklch(0.68 0.11 148)}
.sw__dot--blue{background:oklch(0.68 0.11 238)}
.sw__dot--pink{background:oklch(0.68 0.11 352)}
/* a hairline of its own: the light half is the page's colour in light mode, the dark half in dark mode */
.sw__dot--mono{--edge:inset 0 0 0 1px color-mix(in oklch,var(--ink) 30%,transparent);background:linear-gradient(135deg,oklch(0.92 0.003 250) 50%,oklch(0.30 0.003 250) 50%);box-shadow:var(--edge)}
.sw[aria-pressed="true"] .sw__dot--mono{box-shadow:var(--edge),0 0 0 2px var(--bg),0 0 0 3.5px var(--accent)}
.chip{display:flex;align-items:center;gap:8px;height:24px;padding:0 9px 0 10px;background:var(--accent);color:var(--on-accent);white-space:nowrap;transition:background .25s}
.ft__clear{color:var(--mute);white-space:nowrap;padding:4px 0;transition:color .25s}
.sortb{padding:4px 0;color:var(--mute);transition:color .25s}
.sortb[aria-pressed="true"]{color:var(--accent);cursor:default}
/* Tap areas (as on the viewer's close button): each small control answers beyond its box, invisibly. Header buttons
   to the header's height; the controls row to exactly its own 38px; text links to 40px and more. Sideways only as far
   as half the gap to a neighbour, so no two areas overlap; up and down only as far as the next line of text, so a tap
   or a hover on that text is not taken for the button (the intro's link has 22px free above it and 6px below). */
.hd__view,.sw,.chip,.ft__clear,.sortb,.intro__go,.endp__back,.empty__act{position:relative}
.hd__view::after,.sw::after,.chip::after,.ft__clear::after,.sortb::after,.intro__go::after,.endp__back::after,.empty__act::after{content:'';position:absolute}
.hd__view::after{inset:-13px -7px -14px}
.sw::after{inset:-4px 0}
.chip::after{inset:-7px -5px}
.ft__clear::after{inset:-6.5px -5px}
.sortb::after{inset:-6.5px -7px}
.intro__go::after{inset:-17px -8px -6px}
.endp__back::after{inset:-12px -8px}
.empty__act::after{inset:-13px -10px}

/* the strip's side margin; 20px between the message and its ways out */
.empty{height:100%;box-sizing:border-box;padding:0 24px;display:flex;flex-direction:column;gap:20px;align-items:center;justify-content:center;color:var(--mute)}
.empty__msg{font-stretch:62%;font-weight:800;font-size:clamp(40px,6vw,64px);line-height:.92;word-spacing:.1em;color:var(--ink);text-align:center;text-wrap:balance;max-width:12em} /* .92: two lines of display type clear their descenders, as the month lockup does */
.empty__acts{display:flex;gap:26px 20px;flex-wrap:wrap;justify-content:center}   /* 26px between lines when the two wrap: room for both tap areas */
.empty__act{color:var(--accent)}

/* --- grid view (task 8) --- */
/* --gh: the height of a row of thumbnails. grid.js sets it so that a whole number of 3:2 frames fills the line exactly,
   and a month's short last row then keeps the columns of the rows above it; 112px until it has measured.
   Rows with other shapes in them are stretched to fill the line.
   --sbw: the scrollbar's width, measured by grid.js. The right margin gives it up, so the thumbnails end on the
   header's right edge with the thin scrollbar tucked into the 24px margin (0 where scrollbars overlay). */
.gr{--gh:112px;height:100%;overflow-y:auto;box-sizing:border-box;padding:0 calc(24px - var(--sbw,0px)) 64px 24px;scrollbar-width:thin;scrollbar-color:var(--rule) transparent}
.gr__sec{display:grid;grid-template-columns:minmax(76px,150px) minmax(0,1fr);gap:20px;padding:18px 0 22px;border-top:1px solid var(--rule)}
.gr__sec:first-child{border-top:0}   /* the header's rule is right above it */
.gr__head{position:sticky;top:12px;align-self:start;display:flex;flex-direction:column;gap:8px}
/* margin-top puts the flat tops of the tall letters on the top edge of the first row, as in the strip's month panels.
   -.02em, not the month type's -.007em: this type is small (36 to 56px), and at -.017em the offset was under a pixel
   at 1280px, where the l of "jul" then drew one row low. In a section that starts on a whole pixel the tall letters are then on the row's top edge
   from 900px to 1920px; where a section starts between pixels they are never below it, and at most a device row or
   two above. */
.gr__title{font-stretch:62%;font-weight:800;font-size:clamp(36px,4vw,56px);line-height:.82;letter-spacing:-.03em;margin-top:-.02em}
.gr__row{display:flex;flex-wrap:wrap;align-items:flex-start;gap:6px}
.gr__cell{overflow:hidden;cursor:zoom-in;transition:opacity .3s;scroll-margin:12px}   /* scrolled to on closing the viewer: not flush against the edge */
.gr__cell picture{display:block;width:100%;height:100%}
.gr__cell img{display:block;width:100%;height:100%;object-fit:cover}
.gr__fill{flex:9999 1 0}
.gr__foot{margin:0;padding-top:18px;border-top:1px solid var(--rule)}
/* phones: the month title goes above its photos on one line with the count (two landscapes to a row, three from about 440px) */
@media (max-width:639.98px){
  .gr__sec{grid-template-columns:minmax(0,1fr);gap:10px}
  .gr__head{position:static;flex-direction:row;align-items:baseline;gap:10px}
}
html[data-view="grid"] .tint{opacity:0}
/* A window shorter than 540px in grid view: one scroller, the page. The grid flows in it and the footer is pinned to the
   bottom, so the filters, sort and timeline are always there (in the strip view the page scrolls 150px down to them, as
   the strip has nothing of its own to scroll up and down). The month titles stick to the window's top instead of the grid's. */
@media (max-height:539.98px){
  html[data-view="grid"]{scroll-padding-bottom:156px}   /* scrollIntoView clears the pinned footer */
  html[data-view="grid"] .app{height:auto;min-height:var(--vh);overflow:visible}   /* the app no longer clips: the page scrolls, and the footer below is sticky to it */
  html[data-view="grid"] .gr{height:auto;overflow:visible;padding-bottom:24px}
  html[data-view="grid"] .ft{position:sticky;bottom:0;background:var(--bg)}
}

/* --- viewer (task 9) --- */
@property --vw-c{syntax:'<color>';inherits:false;initial-value:transparent}
.vw{position:fixed;inset:0;background:radial-gradient(70% 80% at 38% 50%,color-mix(in oklch,var(--vw-c) 20%,transparent),transparent 75%),var(--viewer-bg)}
.vw{z-index:50;color:var(--viewer-ink);display:grid;grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto;transition:--vw-c 1.2s ease}
.vw__area{min-width:0;min-height:0;display:flex;align-items:center;justify-content:center;padding:clamp(16px,3vw,40px);padding:clamp(16px,round(3vw,1px),40px);box-sizing:border-box;touch-action:pan-y pinch-zoom;cursor:zoom-out}
.vw__fit{width:100%;height:100%;container-type:size;display:flex;align-items:center;justify-content:center;transition:opacity .22s ease,transform .3s var(--ease)}
/* a hairline so a dark photo keeps its edge */
.vw__frame{position:relative;width:min(100cqw,calc(100cqh * var(--ar)));height:min(100cqh,calc(100cqw / var(--ar)));cursor:default;box-shadow:0 0 0 1px rgba(255,255,255,.07)}
/* Whole pixels where round() is known (.01px so a whole size that arrives as 903.99 stays whole). Behind @supports because a
   declaration with var() in it always parses: without round() it would not fall back to the sizes above but leave the frame 0×0. */
@supports (width:round(down,1px,1px)){
  .vw__frame{width:round(down,calc(min(100cqw,calc(100cqh * var(--ar))) + .01px),1px);height:round(down,calc(min(100cqh,calc(100cqw / var(--ar))) + .01px),1px)}
}
.vw__frame picture,.vw__frame img{position:absolute;inset:0;width:100%;height:100%;display:block}
.vw__frame img{object-fit:cover;-webkit-user-drag:none}
.vw__frame img.rv{transform:none;transition:opacity .5s ease}
.vw__fit.is-quick img.rv{transition:none}   /* flipping fast, or under the opening flight: no fade of its own */
.vw__rail{min-height:0;display:flex;flex-direction:column;justify-content:space-between;gap:20px;padding:20px 24px 24px;border-top:1px solid rgba(255,255,255,.12)}
.vw__top{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.vw__pos{opacity:.6;font-variant-numeric:tabular-nums}
.vw__close{position:relative;display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1.5px solid rgba(255,255,255,.35);transition:border-color .25s}
.vw__close::after{content:'';position:absolute;inset:-10px -12px}   /* a tap area of 44px and more */
.vw__esc{opacity:.6}
.vw__mid{display:flex;flex-direction:column;gap:18px}
.vw__num{font-stretch:62%;font-weight:800;font-size:64px;line-height:.8;letter-spacing:-.03em;font-variant-numeric:tabular-nums;margin-left:-.038em}   /* the numeral's ink on the table's rule: a leading 1 a pixel inside it, a leading 0 two outside */
.spec--dark{border-top-color:rgba(255,255,255,.5)}
.spec--dark > div{border-bottom-color:rgba(255,255,255,.12)}
.spec--dark dt{color:inherit;opacity:.6}
.spec--dark dd{display:flex;align-items:center;gap:8px}
.vw__sw{width:10px;height:10px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(255,255,255,.3)}
.vw__bot{display:flex;flex-direction:column;gap:12px}
.vw__mini-wrap{display:flex;flex-direction:column;gap:6px}
.vw__mini{height:22px;display:flex;align-items:flex-end;cursor:pointer}
.vw__tick{flex:1 1 0;min-width:0;height:100%;opacity:.55;transform:scaleY(.6);transform-origin:bottom;transition:transform .3s ease,opacity .3s ease}
.vw__tick.is-cur{opacity:1;transform:scaleY(1)}
.vw__tick.is-cur{background:var(--accent)!important;box-shadow:-1px 0 0 var(--accent),1px 0 0 var(--accent)}   /* 3px and more, however thin the ticks */
/* at either end the extra goes inwards, so the marker stops on the rail's content edge */
.vw__tick.is-cur:first-child{box-shadow:2px 0 0 var(--accent)}
.vw__tick.is-cur:last-child{box-shadow:-2px 0 0 var(--accent)}
.vw__tick.is-cur:only-child{box-shadow:none}
.vw__small{opacity:.6;font-size:11px}
.vw__nav{display:flex;gap:6px}
.vw__nav button{flex:1;height:44px;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255,255,255,.22);transition:border-color .25s}
.vw__nav button:focus-visible{outline-offset:-5px}
.vw__nav button:disabled{opacity:.35;cursor:default}   /* a list of one: nowhere to step to */
.vw__hint{text-align:center}
/* The rail goes beside the photo only in a window that is wide enough (820px) and wide for its height (at least 6:5):
   in a tall window the side rail would leave the photo small with empty bands above and below it, and the rail below
   gives the photo the whole width. The two blocks partition every window: 6/5 and up is one, 1.1999 and down the other. */
@media (max-width:819.98px),(max-aspect-ratio:11999/10000){
  .vw .wide-only,.vw__hint{display:none!important}
  /* the rail is below the photo here, so the glow sits over the photo: centred, in the upper part */
  .vw{background:radial-gradient(90% 55% at 50% 38%,color-mix(in oklch,var(--vw-c) 20%,transparent),transparent 75%),var(--viewer-bg)}
  .vw__num{font-size:40px}
  .vw__rail{gap:14px;padding:14px 16px 16px}
}
@media (min-width:820px) and (min-aspect-ratio:6/5){
  .vw{grid-template-columns:minmax(0,1fr) 300px;grid-template-rows:minmax(0,1fr)}
  .vw__rail{border-top:0;border-left:1px solid rgba(255,255,255,.12)}
  .vw__num{font-size:132px}
}
/* a wide but short window (a phone on its side): the side rail gives up size and its two hint lines so prev and next stay on screen */
@media (min-width:820px) and (max-height:559.98px){
  .vw__rail{gap:14px;padding:16px 24px}
  .vw__num{font-size:72px}
  .spec--dark > div{padding:5px 0}
  .vw__hint,.vw__mini-wrap .vw__small{display:none}
}
/* no keyboard and no mouse: nothing about esc, arrow keys or clicking */
@media (hover:none){.vw__esc,.vw__hint,.vw__mini-wrap .vw__small{display:none}}

/* --- viewer flight (task 10) --- */
/* the dialog while it fades out on closing; the thumbnail its photo is flying home to */
.vw-back{z-index:55;pointer-events:none}
/* the rail goes first: its text would otherwise sit, legible, on top of the page's own header and footer as they come back.
   The transition is on the closing state only, so a dialog opened again mid-fade has its rail back at once. */
.vw-back .vw__rail{opacity:0;transition:opacity .12s ease}
.is-away{opacity:0!important;transition:none!important}
.vw-ghost{position:fixed;z-index:60;pointer-events:none;overflow:hidden;transform-origin:0 0;will-change:transform}
.vw-ghost img{display:block;width:100%;height:100%;object-fit:cover}

/* --- interaction vocabulary (polish 5): hover, pressed and selected say the same thing on every control ---
   hover   only where a pointer can hover. grey text goes to ink, months to red, red text gains an underline, a solid
           chip darkens, a dot grows, a photo's caption goes red, a thumbnail thins, an outline strengthens.
   pressed everything dims to .7 while held; round controls (dots, arrows) dip instead. At once, with no easing.
           Not behind (hover:hover): a finger presses too. Except on photos and thumbnails, which a finger scrolls by.
   selected is each control's own rule above (red text, red underline, red ring). Hover never overrides it, and a
           disabled control answers to neither. */
/* red text buttons share one underline, there from the start so that only its colour has to change */
.intro__go,.endp__back,.empty__act{text-decoration:underline;text-decoration-color:transparent;text-underline-offset:4px;transition:text-decoration-color .25s,gap .25s}
@media (hover:hover){
  /* quiet text controls: grey to ink (the selected sort stays red) */
  .hd__view:hover,.ft__clear:hover,.sortb:not([aria-pressed="true"]):hover{color:var(--ink)}
  .tl-m:not(:disabled):hover{color:var(--accent)}
  /* red text buttons: underline; the two that point somewhere also open their arrow */
  .intro__go:hover,.endp__back:hover,.empty__act:hover{text-decoration-color:var(--accent)}
  .intro__go:hover,.endp__back:hover{gap:14px}
  /* solid: darken */
  .chip:hover{background:color-mix(in oklch,var(--accent) 86%,var(--ink))}
  /* dots: grow; a swatch dimmed by another colour's filter comes back to full strength under the pointer */
  .sw:not(:disabled):hover .sw__dot{transform:scale(1.15)}
  .sw.is-dim:not(:disabled):hover{opacity:1}
  /* photos */
  .ph:hover{color:var(--accent)}
  .gr__cell:hover{opacity:.8}
  /* pressed, for these two only under a mouse: a finger rests on them to scroll, and would flash each one it touched */
  .ph__box:active,.gr__cell:active{opacity:.7}
  .gr__cell:active{transition-duration:0s}
  .vw__tick:hover{opacity:1;transform:scaleY(1)}
  /* outlines: stronger */
  .arrow:hover{box-shadow:0 0 0 1px var(--ink),0 6px 18px rgba(0,0,0,.12)}
  .vw__close:hover{border-bottom-color:var(--accent)}
  .vw__nav button:not(:disabled):hover{border-color:rgba(255,255,255,.7)}
}
/* pressed. A view or sort that is already selected would do nothing, so it does not answer. */
.hd__view:not([aria-pressed="true"]):active,.sortb:not([aria-pressed="true"]):active,.ft__clear:active,.tl-m:not(:disabled):active,.chip:active,
.intro__go:active,.endp__back:active,.empty__act:active,.vw__close:active,.vw__nav button:not(:disabled):active{opacity:.7}
/* the arrows' opacity belongs to strip.js (it fades them at the ends), so they dip like the dots */
.sw:not(:disabled):active .sw__dot,.arrow:active{transform:scale(.94)}
/* pressed is instant everywhere: a dot's hover growth does not ease the press in (nor a thumbnail's hover fade, above) */
.sw:not(:disabled):active .sw__dot{transition:box-shadow .3s}
