/* Every colour, size and timing on the site lives here.
   Phones and tablets share one layout, up to 1024px wide. CSS cannot read a token inside a media
   query, so that one number also appears in layout.css and in the image sizes in index.html. */
:root {
  /* The site's scale. Laptops and larger draw every fixed size at half, the look of a
     browser zoomed to 50%: sizes set from the window (vw, vh and the container units) stay
     as they are, and every pixel value in this file is multiplied by --site_scale. Phones
     and tablets stay at full size. */
  --site_scale: 1;
  --small_text_min: 10.5px;         /* captions, labels and the tab names never go below this, even at half scale */
  --body_text_min: 12px;            /* nor body text below this */

  /* colour */
  --brand_white: #E8EAE5;
  --charcoal: #2A2D2A;
  --pale_yellow: #DFE0C1;
  --warm_brown: #87817B;
  --sage: #93A997;
  --blue_grey: #8CA3A8;

  /* type */
  --font: 'Satoshi', system-ui, sans-serif;
  --weight_light: 300;
  --weight_regular: 400;
  --weight_medium: 500;
  --weight_bold: 700;
  --weight_black: 900;
  --text_small: max(0.8125rem, var(--small_text_min));
  --text_body: max(1.0625rem, var(--body_text_min));
  --text_lead: clamp(calc(26px * var(--site_scale)), 2.36vw, calc(36px * var(--site_scale)));   /* lead paragraphs: about double the body size (34px on a 1440 laptop). Phones: set at the end */
  --line_lead: 1.25;
  --text_name: 1.375rem;
  --text_large: clamp(1.5rem, 3vw, 2.25rem);
  --text_title: clamp(2.5rem, 7vw, 5.5rem);
  --line_body: 1.5;
  --line_tight: 1.1;

  /* spacing */
  --space_1: calc(4px * var(--site_scale));
  --space_2: calc(8px * var(--site_scale));
  --space_3: calc(16px * var(--site_scale));
  --space_4: calc(24px * var(--site_scale));
  --space_5: calc(40px * var(--site_scale));
  --space_6: calc(64px * var(--site_scale));
  --hero_gap: clamp(calc(12px * var(--site_scale)), 1.8vw, calc(28px * var(--site_scale)));
  --sheet_padding: clamp(calc(20px * var(--site_scale)), 4vw, calc(64px * var(--site_scale)));
  --line_width: calc(1px * var(--site_scale));
  --focus_width: calc(2px * var(--site_scale));

  /* banner rail. Proportions follow reference/brand_boards/banners.png.
     Strip size: phone and tablet below, laptop in the media query at the end. */
  --rail_count: 3;
  --rail_strip: calc(53px * var(--site_scale));   /* phones and tablets. Never below 44px, the smallest comfortable tap height */
  --rail_width: calc(var(--rail_strip) * var(--rail_count));
  --banner_mark_height: calc(var(--rail_strip) * .84);
  --monogram_ratio: 330.8 / 402.24;   /* monogram.svg, used on the banners */
  --banner_label_size: calc(var(--rail_strip) * .8);
  --banner_mark_gap: var(--rail_strip);
  --banner_unit_gap: calc(var(--rail_strip) * 1.25);
  --marquee_speed: 22;   /* pixels per second */

  /* hero crop on a phone or tablet: which part of the photo stays in frame,
     across then down. 0% shows the top of the photo, 100% the bottom. */
  --hero_focus: 50% 50%;

  /* hero crop on a laptop: a row of the photo is pinned to a height in the frame,
     so it stays put however wide or tall the window is. Both run from 0 (top) to 1 (bottom). */
  --hero_pin_photo: 0.71;   /* the row of the photo to hold still: the cars */
  --hero_pin_frame: 0.795;  /* where that row sits in the frame */
  --hero_ratio: 1.3333;     /* the photo's height divided by its width (2400 / 1800) */

  /* hero type overlay. Sized from the hero image (a CSS container), so it keeps the
     same share of the image at every size. */
  --hero_type_colour: var(--brand_white);
  --hero_type_leading: 0.9;
  --hero_type_tracking: -0.02em;
  --hero_type_lines: 7;
  --hero_type_measure: 10.12;      /* width of the widest line (GRAPHIC DESIGNER.), in em. Re-measure if the words change */
  --hero_type_pad: 0.3;            /* space round the block, in em, about the width of the image border */
  --hero_type_width: 1;            /* share of the image width the block may span. 1 fills it */
  --hero_type_min: calc(14px * var(--site_scale));
  --hero_type_max: calc(264px * var(--site_scale));
  --hero_type_delay: 200ms;        /* the beat after the swipe finishes */

  /* titles: the project name on each project page, and the top text on About Me (the
     .title_type class in base.css). Satoshi Black at every size. */
  --title_weight: var(--weight_black);
  --title_case: uppercase;
  --title_tracking: var(--hero_type_tracking);
  --title_leading: var(--hero_type_leading);

  /* hero type scramble: each letter cycles random characters, then locks left to right.
     Last line starts 6 x line stagger in, plus the hold, plus 16 letters: about 2.2s in all */
  --scramble_tick: 90ms;           /* how often a scrambling letter changes */
  --scramble_hold: 600ms;          /* how long a line scrambles before its first letter locks */
  --scramble_letter: 45ms;         /* between one letter locking and the next */
  --scramble_line: 160ms;          /* between one line starting and the next */

  /* MY WORK layout grid (not drawn): 4 columns on a phone, 8 on a tablet, 12 on a laptop
     (set in the media queries at the end). Margins and gutters match the hero border. */
  --grid_columns: 4;
  --grid_margin: var(--hero_gap);
  --grid_gutter: var(--hero_gap);

  /* About Me photo */
  --about_photo_ratio: 4 / 5;       /* assets/about/about_me is 1920 by 2400 */
  --about_photo_tall: 1.25;         /* its height as a multiple of its width (2400 / 1920) */
  --about_text_min: 16em;           /* narrowest the copy column beside the photo gets (24em from 600px, below); the photo shrinks to leave it room */
  --about_label_tracking: 0.2em;    /* the CV link, a small spaced uppercase label */
  --about_title_measure: 9;         /* the top text: its column's width in ems, so the longest word (COMMUNICATION, 8.9em in Black) just fits */
  --about_title_leading: 1;         /* the top text's line height: a touch looser than a project name's 0.9, still tight */
  --about_title_gap: 0.5em;         /* between its three thoughts: about half a line */
  --about_title_min_size: calc(16px * var(--site_scale));     /* the top text never shrinks below this to fit one screen; a very short screen scrolls instead */
  --about_body_min_measure: 14;     /* the set top text is only used if each body paragraph under it stays at least this many ems wide */
  --about_line_spacing_min: calc(-1px * var(--site_scale));   /* the top text set with hand breaks: the tightest letter spacing a line may get */
  --about_line_spacing_max: calc(4px * var(--site_scale));    /* and the loosest. If no box width keeps every line within these, the text wraps normally */
  --about_title_share: 0.95;        /* laptops and tablets: the top text's box, as a share of the copy column's width */
  --about_section_space: var(--space_5);   /* between the top text and the body (and the photo, on a phone) */
  --about_paragraph_space: var(--space_4); /* between the two body paragraphs */
  --about_body_measure: 30em;       /* the body's longest line: about 60 to 65 characters */
  --about_body_column_gap_phone: var(--space_3);
  --about_photo_cap_phone: 38cqh;   /* phone: the photo's height at most, so the top text starts in the first screen */
  --about_photo_focus_phone: 50% 64%;   /* phone: the part of the photo kept when its height is capped, centred on Harvey's upper body so his face shows */
  --about_title_size_phone: calc((100cqi - var(--hero_gap) * 2) / 17);   /* phone: the top text, wrapping normally, so most of it shows in the first screen */   /* phone: between the two body paragraphs, side by side */
  --about_body_column_gap: var(--space_5);   /* laptops: between the two body paragraphs, side by side */
  --about_cv_space: var(--space_5); /* above the CV link */
  --about_banner_gap: var(--space_2);   /* laptop: how close the copy's column runs to the MY WORK banner */
  --about_fade_duration: 600ms;

  /* Contact: the photo in the right column, the three lines bottom left like a project name */
  --contact_photo_ratio: 3 / 4;
  --contact_photo_focus_phone: 30% 62%;   /* phone: the part of the photo kept when it is shortened, Harvey and the peaks */     /* assets/contact/contact_photo is 1500 by 2000 */
  --contact_lines_share: 0.48;      /* laptop: the lines' column is never narrower than this share of the page, so the lines can be large */
  --contact_label_space: var(--space_2);   /* between a small label and its line */
  --contact_item_space: var(--space_5);    /* between one contact and the next */
  --contact_lines_delay: 900ms;     /* the lines start as the page lands (the same as --swipe_duration), once the photo has faded in */
  --contact_line_stagger: 140ms;    /* and arrive this far apart */
  --contact_underline: calc(2px * var(--site_scale));         /* a link's hover underline */
  --contact_underline_gap: 0.04em;  /* how far below the line it sits */
  --contact_underline_duration: 450ms;   /* drawing in from the left */
  --about_fade_delay: calc(var(--rail_duration) - var(--about_fade_duration));   /* ends as the page lands */

  /* MY WORK thumbnails: a row along the bottom border margin, looping sideways. The
     project colours live with each project in scripts/projects.js. */
  --work_thumb_share: 0.5;          /* each image's height as a share of the page height */
  --work_thumb_ratio: 0.8;          /* image width as a share of its height: 4:5 */
  --work_thumb_phone: 0.6;          /* phone: each thumbnail's width as a share of the screen, so the next peeks in */
  --work_gap: calc(12px * var(--site_scale));                 /* between thumbnails, the same on every screen */
  --work_tab_text: max(clamp(calc(14px * var(--site_scale)), 1.2vw, calc(17px * var(--site_scale))), var(--small_text_min));   /* the name in each tab: 14px on a phone, 17px on a laptop */
  --work_tab_height: 2;             /* the tab, as a multiple of its text size */
  --work_card_border: var(--space_2);   /* the project colour showing round each image at the sides and bottom */
  --work_tab_pad: var(--work_card_border);   /* the name lines up with the image's left edge */
  --project_cover_shade: 12%;       /* how much darker than the project colour the space is until a cover exists */

  /* Scrolling the row */
  --work_glide: 0.14;               /* share of the way to its target the row moves each frame: lower glides longer */
  --work_fling: 260;                /* after a drag, how far the row keeps travelling, in ms at the release speed */
  --work_drag_threshold: calc(6px * var(--site_scale));       /* a press moving further than this is a drag, not a click */
  --work_wheel_line: calc(16px * var(--site_scale));          /* one line of a line based mouse wheel */
  --work_stretch_max: 0.05;         /* the most a thumbnail stretches while moving fast: 5% */
  --work_stretch_per_speed: 0.012;  /* stretch per pixel per millisecond of speed */
  --work_stretch_follow: 0.15;      /* how quickly the stretch follows the speed, each frame */

  /* The deal, each time MY WORK opens */
  --work_deal_gap: 200ms;           /* between one thumbnail landing and the next */
  --work_deal_drop: 420ms;          /* each one dropping onto the pile */
  --work_deal_scale: 0.4;           /* their size on the pile */
  --work_deal_lift: calc(24px * var(--site_scale));           /* how far above the pile each one starts */
  --work_deal_tilt: 3deg;           /* the most each one lands turned */
  --work_spread: 1400ms;             /* all of them spreading out to their places */

  /* Opening a project, each step as the one before lands: the other thumbnails shrink
     away, the name fades out of its tab, the tab grows over the screen (out to both
     edges, then up and down), the image travels to its place, the name decodes, and the
     description, year and category fade in. */
  --work_shrink: 450ms;             /* the other thumbnails shrinking away, or growing back */
  --project_open_stage: 600ms;      /* each colour stage: across, then up and down (one stage on a phone) */
  --project_fly: 900ms;             /* the image travelling from the row to the page */

  /* An open project's layout: 12 columns on laptops and tablets (one on a phone, below) */
  --project_view_columns: 12;
  --project_image_column: 7;        /* the image fills columns 7 to 12 */
  --project_name_columns: 6;        /* the name, description, year and category sit in columns 1 to 6 */
  --project_details_second: 4;      /* the category column starts at column 4 */
  --project_title_line: 7.79;       /* the widest line of the longest name as it actually sets (COULD SPEAK…), in em: the measure above rounds it up a touch */
  --project_title_measure: 7.85;    /* widest line of the longest name (COULD SPEAK…), in em. Re-measure if the names change */
  --project_name_gap: var(--space_2);   /* between the name and the description */
  --project_paragraph_gap: 1em;     /* between paragraphs of a project's large text */
  --project_description_gap: var(--space_4);   /* between the description and the year and category */
  --project_section_space: clamp(calc(80px * var(--site_scale)), 10vw, calc(160px * var(--site_scale)));   /* below the first screen and between the further sections, the same each time: 144px on a 1440 laptop */
  --project_section_stack: var(--space_4);   /* phone: between a section's text and its image, stacked */

  /* A pinned project page (Tastebud): the name stays pinned bottom left while the right
     column scrolls past, and its large text reveals line by line */
  --pinned_split: 0.57;             /* the right column starts at this share of the page width */
  --lead_optical_shift: -0.06em;    /* tablets and laptops: the lead text under the cover moves left by its first capital's side bearing, so its ink lines up with the cover's edge */

  /* The alignment lines, only on a project that sets align: 'lines' (Pae and Homesick).
     On tablets and laptops every image, video, text block and caption there starts and
     ends on one of four lines: 1 the left margin, 2 the end of the left zone (the split
     minus one gutter), 3 the split, 4 the right margin. The gutter and the stack gap
     scale with the page like the margin. */
  --line_gutter: clamp(calc(11px * var(--site_scale)), 1.6667vw, calc(26px * var(--site_scale)));   /* between the left zone and the right column: 24px on a 1440 laptop */
  --stack_gap: clamp(calc(18px * var(--site_scale)), 2.7778vw, calc(43px * var(--site_scale)));     /* between items stacked inside a section: 40px on a 1440 laptop */
  --series_lines_gap: var(--line_gutter);       /* between images in a series, across and down: one gutter */
  --layout_reference: 1440;
  --video_loop_pause: 1000ms;       /* every project video holds its last frame this long before playing again */
  --slot_border: calc(1px * var(--site_scale));               /* a photo mockup still to come: its dashed outline */
  --slot_opacity: 0.55;             /* and how strongly the outline and its label show */

  /* The logo reveal (NorthStar): the mark draws, fills, then the letters settle in */
  --logo_reveal_width: 58%;         /* the logo's width on its panel */
  --logo_stroke: 3;                 /* the drawing line, in the logo's own units */
  --logo_draw_duration: 1800ms;     /* the mark's outline drawing in */
  --logo_fill_duration: 700ms;      /* then its fill */
  --logo_letter_duration: 900ms;    /* each letter settling in */
  --logo_letter_stagger: 70ms;      /* letters arrive this far apart */
  --logo_letter_rise: calc(28px * var(--site_scale));         /* each letter rises this far, in the logo's own units */
  --ease_in_out: cubic-bezier(0.65, 0, 0.35, 1);   /* weighted, no bounce */         /* a spread's offsets are given in pixels at this page width, and scale with it */
  --pinned_text_gap: var(--space_5);   /* between the cover and the description beneath it */
  --pinned_lead_scale: 0.29;        /* the large text, as a share of the project name's size: about eight to ten words a line on a laptop */
  --pinned_lead_min: calc(17px * var(--site_scale));          /* the large text never goes smaller than this (a tablet's narrow column) */
  --pinned_lead_weight: var(--weight_bold);
  --pinned_lead_tracking: -0.015em; /* a little tighter, to suit the heavier weight */
  --pinned_lead_scale_phone: 0.27;  /* phone: both large texts, about 16px on a 390 phone */
  --pinned_lead_min_phone: calc(15px * var(--site_scale));    /* phone: never smaller than this */
  --pinned_lead_leading: 1.1;
  --pinned_billboard_gap: clamp(calc(120px * var(--site_scale)), 14vw, calc(240px * var(--site_scale)));   /* after the name releases, before the billboard */
  --pinned_intro_close_gap: clamp(calc(60px * var(--site_scale)), 7vw, calc(120px * var(--site_scale)));   /* a project with introClose: the sections start this far under the first screen, 101px on a 1440 laptop */
  --pinned_section_gap: clamp(calc(120px * var(--site_scale)), 13vw, calc(230px * var(--site_scale)));   /* above the design challenge: 187px on a 1440 laptop */
  --pinned_challenge_lift: clamp(calc(24px * var(--site_scale)), 3vw, calc(48px * var(--site_scale)));   /* the design challenge label and text sit this much higher, with nothing below moving: 43px on a 1440 laptop */
  --pinned_close_gap: clamp(calc(48px * var(--site_scale)), 6vw, calc(100px * var(--site_scale)));   /* a section marked closer sits this far below the caption of the one before (a pair's taller right image is allowed to hang down beside it): 86px on a 1440 laptop */
  --pinned_image_gap: clamp(calc(48px * var(--site_scale)), 5vw, calc(90px * var(--site_scale)));   /* between the design challenge text and the laptop image below it: 72px on a 1440 laptop */
  --pinned_lower_gap: clamp(calc(130px * var(--site_scale)), 14vw, calc(250px * var(--site_scale)));   /* above the animation and the mockup pair: 202px on a 1440 laptop */
  --caption_space: var(--space_3);  /* between an image and its caption */
  --caption_number_width: 4.3em;    /* a caption's number and the wide gap after it */
  --billboard_ratio: 2400 / 1339;   /* the Tastebud billboard; the animation matches it */
  --image_drop: clamp(calc(80px * var(--site_scale)), 12vw, calc(200px * var(--site_scale)));   /* a dropped image sits this much lower than its text: 173px on a 1440 laptop */
  --image_lift_more: clamp(calc(60px * var(--site_scale)), 34vw - calc(170px * var(--site_scale)), calc(360px * var(--site_scale)));   /* lift: 'more', 320px on a 1440 laptop, less on smaller screens where there is less room */
  --text_block_scale: 1.5;          /* a full width text block, as a multiple of the large text size */
  --text_block_weight: var(--weight_black);
  --stagger_text_gap: clamp(calc(64px * var(--site_scale)), 7vw, calc(120px * var(--site_scale)));   /* editorial paragraphs: the space between them, 101px on a 1440 laptop */
  --stagger_text_width: 0.85;       /* and each one's width, as a share of the column (when not set by its length) */
  --stagger_text_min: 0.7;          /* the narrowest a paragraph gets: longer ones widen towards the full column */
  --stagger_pull: 0.5;              /* beside a filled image, the left hand ones reach this share of the way towards it */
  --image_lift: clamp(calc(60px * var(--site_scale)), 8vw, calc(140px * var(--site_scale)));   /* a lifted image rises this far above its text: 115px on a 1440 laptop */
  --series_rows_count: 4;           /* a series in rows: images per row */
  --series_rows_count_phone: 2;     /* and on a phone, stacked in pairs that fit the screen */
  --series_rows_gap: var(--space_4); /* and the space between its images, across and down */
  --series_gap: var(--space_2);     /* between images in a series */
  --series_stagger: 80ms;           /* a series' images reveal this far apart, left to right */
  --series_phone_width: 0.6;        /* phone: each series image's width, as a share of the screen */
  --pair_tuck: clamp(calc(40px * var(--site_scale)), 7vw, calc(120px * var(--site_scale)));   /* a tucked pair's left image starts this far above the bottom of the image before it: 100px on a 1440 laptop */
  --pair_portrait_left_scale: 0.744; /* a pair of two portraits: the left one, as a share of the left zone */
  --pair_portrait_stagger: 0.3;     /* and the right one starts this share of the way down the left one */
  --pair_left_scale: 0.9;           /* a mockup pair's landscape image, as a share of the pinned page's left zone */
  --pair_right_start: 0.71;         /* its portrait image's starting width (from this share of the page width), before it is scaled up to line up with the top */
  --pair_right_phone: 1;            /* phone: its width, as a share of the screen inside the margins */
  --section_label_scale: 1.5;       /* a section label (DESIGN CHALLENGE:), as a multiple of the year and category size */
  --section_then_space: clamp(calc(96px * var(--site_scale)), 11vw, calc(180px * var(--site_scale)));   /* a follow on labelled block (Chockie's DESIGN CHALLENGE:) sits this far under the text before it: 158px on a 1440 laptop */
  --section_label_space: var(--space_3);   /* between a section's small label and its text */
  --line_reveal_duration: 900ms;    /* each line sliding up from behind its mask */
  --line_reveal_stagger: 90ms;      /* between lines arriving together */
  --line_reveal_threshold: 0.6;     /* how much of a line must be on screen before it slides up */
  --line_reveal_from: 130%;         /* where a line starts, below its mask */
  --line_mask_bleed: 0.2em;         /* the mask reaches this far below the line, so descenders show */

  /* Closing a project: what is on screen leaves first (about 450ms), then the empty colour
     shrinks back into its card */
  --close_content_duration: 300ms;  /* each piece of content leaving */
  --close_line_stagger: 15ms;       /* between lines of large text sliding back down */
  --close_line_stagger_max: 90ms;   /* the last line starts no later than this */
  --close_name_delay: 150ms;        /* the name and its labels start this much later, so they go last */
  --close_fade_reduced: 150ms;      /* reduced motion: the content simply fades this fast */

  /* Revealing a project's further sections as they scroll into view */
  --reveal_duration: 800ms;
  --reveal_rise: calc(32px * var(--site_scale));              /* how far an image rises as it fades in */
  --reveal_stagger: 250ms;          /* the beat between a section's text and its image */
  --reveal_threshold: 0.2;          /* how much of a section must be on screen before it reveals */
  --project_detail_size: var(--text_small);
  --project_detail_leading: 1.15;
  --project_details_stack: var(--space_2);   /* phone: between the year and the category */

  /* An open project below 768px: one column, the image on top, the text underneath */
  --project_margin_mobile: calc(16px * var(--site_scale));    /* the gutter either side */
  --project_title_measure_mobile: 6.05;   /* widest single word of any name (NORTHSTAR) in Satoshi Black, in em. Re-measure if the names change */
  --project_title_space_mobile: calc(32px * var(--site_scale));     /* between the image and the name */
  --project_details_space_mobile: var(--space_4);   /* between the name and the description */
  --project_details_stack_mobile: var(--space_3);   /* between the year and the category */
  --project_detail_size_mobile: var(--text_body);
  --project_gallery_space: var(--space_6);   /* between the text and the further images */
  --project_gallery_gap: var(--project_margin_mobile);   /* between further images */
  --project_close_size: calc(44px * var(--site_scale));       /* the close button, at least 44px to tap */
  --project_close_cross: 45%;       /* the X, as a share of the button */
  --project_close_line: calc(1.5px * var(--site_scale));      /* thickness of the two lines in the X */
  --project_close_clear: calc(0px * var(--site_scale));       /* the image starts this far below the top margin: none, it starts on the margin with the close button over it */
  --project_close_layer: 10;        /* the close button sits above the image and all page content */

  /* MY WORK wordmark, from reference/wordmark_loop.html. Sized from the open page area */
  --wordmark_colour: var(--charcoal);
  --wordmark_layout_size: 50px;     /* type size the letters are measured and laid out at, then scaled to fit: one fixed size on every screen, so a window resized after loading never leaves the letters spaced for a different size */
  --wordmark_columns: 1.4;          /* how many grid columns it spans at its widest, centred at the top */
  --wordmark_hold: 2400ms;          /* each lockup stays still */
  --wordmark_move: 1400ms;          /* letters sliding along their row */
  --wordmark_settle: 900ms;         /* rows joining or separating */
  --wordmark_row_stagger: 60ms;     /* between rows starting to slide */

  /* hero paper texture and wash. Each runs from 0 (none) to 1 (full) */
  --hero_wash: 0.2;          /* lifts the images toward brand white */
  --hero_desaturate: 0.3;    /* takes colour out of the images */
  --paper_strength: 0.55;    /* how strongly the paper grain shows */
  --paper_size: calc(220px * var(--site_scale));       /* size of one tile of grain */
  --paper_texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix values='.33 .33 .33 0 0 .33 .33 .33 0 0 .33 .33 .33 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23p)'/%3E%3C/svg%3E");

  /* project tiles */
  --tile_min: calc(220px * var(--site_scale));
  --photo_ratio: 4 / 5;

  /* layers */
  --layer_rail: 20;
  --layer_project: 50;              /* an open project covers the rail and the wordmark */
  --layer_loader: 100;

  /* motion */
  --ease: cubic-bezier(.65, 0, .35, 1);
  --marquee_ease: linear;   /* the one exception: a loop has to run at a steady speed */
  --swipe_duration: 900ms;
  --rail_duration: var(--swipe_duration);   /* banners sliding across and back */
  --fade_duration: 300ms;

  /* loader timings, copied from reference/hwgd_monogram_intro.html */
  --loader_mark_height: min(44vh, 70vw);
  --loader_reveal: 2600ms;
  --loader_pupils_at: 2650ms;
  --loader_pupil_pop: 220ms;
  --loader_look_at: 2800ms;
  --loader_glance_1: 0ms;
  --loader_glance_2: 450ms;
  --loader_glance_3: 850ms;
  --loader_dart: 130ms;
  --loader_settle_at: 4050ms;
  --loader_settle: 650ms;
  --loader_blink_1_at: 4900ms;
  --loader_blink_2_at: 5260ms;
  --loader_blink: 280ms;
  --loader_end: 5700ms;
}

/* Laptop: 98px strips on a 1440 wide screen, scaling down with the screen on
   smaller laptops. */
/* Phones: lead paragraphs scale with the screen, so a line still holds four or five words. */
@media (max-width: 599px) {
  :root { --text_lead: clamp(calc(19px * var(--site_scale)), 5.4vw, calc(24px * var(--site_scale))); }
}

/* Below 768px: an open project stacks in one column, its name the full width. */
@media (max-width: 767px) {
  :root {
    --project_view_columns: 1;
    --project_image_column: 1;
    --project_name_columns: 1;
  }
}

@media (min-width: 600px) {
  :root {
    --about_text_min: 24em;         /* tablets and laptops: the copy column beside the photo is never narrower, the photo shrinks instead */
    --grid_columns: 8;
  }
}

@media (min-width: 1025px) {
  :root {
    --about_text_min: 34em;         /* the copy column, wide enough for its two body paragraphs side by side: the photo shrinks to give it room */
    --rail_strip: clamp(calc(65px * var(--site_scale)), 6.8vw, calc(98px * var(--site_scale)));
    --grid_columns: 12;
  }
}

/* Laptops and larger: half scale (see --site_scale above). */
@media (min-width: 1025px) {
  :root { --site_scale: 0.5; }
}

/* Pixel tokens the scripts read are registered as lengths, so the scripts are given the
   finished scaled size (8px) rather than the formula. */
@property --about_title_min_size { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --about_line_spacing_min { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --about_line_spacing_max { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --work_gap { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --work_drag_threshold { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --work_wheel_line { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --work_deal_lift { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --reveal_rise { syntax: '<length>'; inherits: true; initial-value: 0px; }
@property --wordmark_layout_size { syntax: '<length>'; inherits: true; initial-value: 0px; }
/* Reset and global styles. */
*, *::before, *::after { box-sizing: border-box; }

html, body { margin: 0; height: 100%; }

/* Sizes in rem follow the site's scale (half on laptops, see --site_scale in tokens.css). */
html { font-size: calc(100% * var(--site_scale)); }

body {
  background: var(--brand_white);
  color: var(--charcoal);
  font-family: var(--font);
  font-weight: var(--weight_regular);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

img, picture, canvas { display: block; max-width: 100%; }

/* Titles: the project name on each project page and the top text on About Me share
   this look. Its values are the --title_ tokens in tokens.css; the size is set where
   each is used. */
.title_type {
  font-family: var(--font);
  font-weight: var(--title_weight);
  line-height: var(--title_leading);
  letter-spacing: var(--title_tracking);
  text-transform: var(--title_case);
}

.visually_hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Hero, banner rail and page sheets. Phone and tablet layout up to 1024px wide, see tokens.css. */

/* ---------- Hero ----------
   One image. On a laptop it stops one border width clear of the banners on the right.
   On a phone or tablet it runs under the banners to the bottom border. */

.hero {
  position: fixed;
  inset: 0;
  display: flex;
  padding: var(--hero_gap);
  padding-right: calc(var(--rail_width) + var(--hero_gap));
}

.hero_image {
  position: relative;
  flex: 1;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  container-type: size;   /* the type and the laptop crop are sized from the image */
}

.hero_image picture { height: 100%; }

.hero_image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero_focus);
}

/* The hero, About Me and project photos share a printed look: colour lowered, washed toward brand
   white, with a fine paper grain over the top. */
.hero_image img,
.about_photo img { filter: saturate(calc(1 - var(--hero_desaturate))); }

/* Project covers the same, unless a project keeps more of its colour (coverSaturation in
   scripts/projects.js). */
.project_view_image img,
.work_image img { filter: saturate(var(--cover_saturation, calc(1 - var(--hero_desaturate)))); }

/* Laptop: the frame is wider than the photo's shape, so the photo is scaled to the
   frame's width and its height runs over. Shift it so the pinned row of the photo
   lands at the pinned height in the frame, never leaving a gap at the top or bottom. */
@media (min-width: 1025px) {
  .hero_image img {
    object-position: 50% min(0px, max(
      calc(100cqh - 100cqw * var(--hero_ratio)),
      calc(100cqh * var(--hero_pin_frame) - 100cqw * var(--hero_ratio) * var(--hero_pin_photo))
    ));
  }
}

/* Wash: a layer of brand white lifts the image. */
.hero_image::before,
.hero_image::after,
.about_photo::before,
.about_photo::after,
.project_view_image::before,
.project_view_image::after,
.work_image::before,
.work_image::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.hero_image::before,
.about_photo::before,
.project_view_image::before,
.work_image::before {
  background: var(--brand_white);
  opacity: var(--hero_wash);
}

/* Paper: fine grain blended in with soft light, so it adds texture without darkening. */
.hero_image::after,
.about_photo::after,
.project_view_image::after,
.work_image::after {
  background-image: var(--paper_texture);
  background-size: var(--paper_size);
  mix-blend-mode: soft-light;
  opacity: var(--paper_strength);
}

/* ---------- Hero type ----------
   Large type printed straight onto the photo, above the paper and wash so it stays
   crisp. Everything is measured from the image, so the block keeps the same share of
   it at every size. The font size is the smaller of two fits, one for the width and
   one for the height, held between a minimum and a maximum. */

/* The type size is worked out on the image. These are evaluated where they are used,
   against the image. */
.hero_image {
  --type_pads: calc(var(--hero_type_pad) * 2);
  /* Width fit: the widest line spans a share of the image, inside the padding. */
  --type_fit_w: min(
    calc(100cqi * var(--hero_type_width) / var(--hero_type_measure)),
    calc(100cqi / (var(--hero_type_measure) + var(--type_pads)))
  );
  /* Height fit: every line and the padding fit the height of the image. */
  --type_fit_h: calc(100cqh / (var(--hero_type_lines) * var(--hero_type_leading) + var(--type_pads)));
  --type_size: clamp(var(--hero_type_min), min(var(--type_fit_w), var(--type_fit_h)), var(--hero_type_max));
  --type_clear_bottom: 0px;   /* room kept clear at the bottom of the image */
}

.hero_type {
  position: absolute;
  top: calc(var(--hero_type_pad) * 1em);
  left: calc(var(--hero_type_pad) * 1em);
  z-index: 1;
  color: var(--hero_type_colour);
  font-size: var(--type_size);
  font-weight: var(--weight_black);
  line-height: var(--hero_type_leading);
  letter-spacing: var(--hero_type_tracking);
  pointer-events: none;
}

/* Stacked as flex columns so the lines' negative margins add up instead of merging,
   which keeps the line spacing exactly as set. */
.hero_type,
.hero_type_name,
.hero_type_rest {
  display: flex;
  flex-direction: column;
}

.hero_type_name,
.hero_type_rest {
  margin: 0;
  font: inherit;
}

/* Each line decodes from random characters (see scripts/scramble.js). While it does,
   every letter sits in a fixed slot the width of its final letter, so the line never
   changes width. The type stays hidden until the swipe has revealed the hero. */
.hero_line {
  display: block;
  white-space: nowrap;
}

.hero:not(.is_revealed) .hero_type { visibility: hidden; }

.hero_slot {
  display: inline-block;
  text-align: center;
  white-space: pre;
}

.hero_slot.is_waiting { visibility: hidden; }
.hero_slot.is_locked { text-align: left; }

/* ---------- Banner rail ----------
   The rail covers the screen but only its banners and open sheet take clicks.
   Each banner carries its sheet on its trailing side, so moving the banner drags its
   colour across the screen. Banners rest at the right edge (bottom edge on a phone or
   tablet). Opening a page moves the chosen banner and those ahead of it, to its left
   (above it on a phone or tablet), to the far edge. */

.rail {
  position: fixed;
  inset: 0;
  z-index: var(--layer_rail);
  container-type: size;
  pointer-events: none;
}

/* Order from the middle of the screen out to the edge: brown, yellow, white. */
.rail_item_contact { --i: 0; }
.rail_item_about { --i: 1; }
.rail_item_work { --i: 2; }

.rail_item {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(100cqw - var(--rail_width) + var(--rail_strip) * var(--i));
  width: var(--rail_strip);
  z-index: calc(1 + var(--i));   /* each banner sits above the sheets of the banners before it */
  transition: transform var(--rail_duration) var(--ease);
}

.rail_item.is_moved { transform: translateX(calc(var(--rail_width) - 100cqw)); }

.banner {
  position: absolute;
  inset: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: hidden;
  container-type: size;
  color: var(--charcoal);
  font: inherit;
  cursor: pointer;
  pointer-events: auto;
}

.banner_work { background: var(--brand_white); }
.banner_about { background: var(--pale_yellow); }
.banner_contact { background: var(--warm_brown); }

.banner:focus-visible {
  outline: var(--focus_width) solid var(--charcoal);
  outline-offset: calc(var(--focus_width) * -2);
}

/* The inner band is laid out as a horizontal strip, then turned to run up the rail. */
.banner_inner {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100cqh;
  height: 100cqw;
  display: flex;
  align-items: center;
  overflow: hidden;
  transform-origin: top left;
  transform: rotate(-90deg);
}

.banner_track {
  display: flex;
  align-items: center;
  flex: none;
  animation: marquee var(--track_duration, 60s) var(--marquee_ease) infinite;
}

.banner_about .banner_track { animation-direction: reverse; }

.banner_unit {
  display: flex;
  align-items: center;
  flex: none;
  gap: var(--banner_mark_gap);
  padding-right: var(--banner_unit_gap);
}

.banner_mark {
  height: var(--banner_mark_height);
  width: auto;
  aspect-ratio: var(--monogram_ratio);
}

.banner_label {
  font-size: var(--banner_label_size);
  font-weight: var(--weight_medium);
  line-height: 1;
  white-space: nowrap;
}

@keyframes marquee { to { transform: translateX(-50%); } }

/* ---------- Page sheets ---------- */

.sheet {
  position: absolute;
  top: 0;
  left: 100%;
  width: 100cqw;
  height: 100cqh;
  color: var(--charcoal);
  visibility: hidden;
}

.sheet.is_shown {
  visibility: visible;
  pointer-events: auto;
}

/* Only the part between the two groups of banners shows, so the text sits there. */
.sheet_scroll {
  position: absolute;
  top: 0;
  right: var(--rail_width);
  bottom: 0;
  left: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sheet_padding);
  container-type: inline-size;   /* the MY WORK wordmark is sized from the page width */
}

.sheet:focus { outline: none; }

/* ---------- MY WORK ----------
   A row of project thumbnails along the bottom border margin, looping sideways (see
   scripts/work_row.js), with the wordmark in the top right. The page is laid out on the
   grid, with margins and gutters matching the hero border. */

.sheet_work .sheet_scroll {
  padding: 0;
  overflow: hidden;
  container-type: size;
}

.work {
  position: relative;
  height: 100%;
  container-type: size;   /* the thumbnails and the wordmark are sized from the page */

  /* one grid column */
  --work_column: calc(
    (100cqi - var(--grid_margin) * 2 - var(--grid_gutter) * (var(--grid_columns) - 1))
    / var(--grid_columns)
  );
  /* each thumbnail's image: about half the page height, 4:5 */
  --work_image_h: calc(100cqh * var(--work_thumb_share));
  --work_image_w: calc(var(--work_image_h) * var(--work_thumb_ratio));
  /* each card: the image with a border of the project colour at the sides and bottom */
  --work_thumb_w: calc(var(--work_image_w) + var(--work_card_border) * 2);
  --work_tab_h: calc(var(--work_tab_text) * var(--work_tab_height));
}

/* The row runs between the side margins and clips there, but not above, so the deal can
   play in the middle of the page. */
.work_row {
  position: absolute;
  right: var(--grid_margin);
  bottom: var(--grid_margin);
  left: var(--grid_margin);
  height: calc(var(--work_tab_h) + var(--work_image_w) / var(--work_thumb_ratio) + var(--work_card_border));
  overflow-x: clip;
  overflow-y: visible;
  touch-action: pan-y;   /* sideways swipes move the row */
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}

.work_row.is_dragging { cursor: grabbing; }

.work_track { position: absolute; inset: 0; }

/* Each slot is placed along the row by the script. The card inside it carries the deal,
   the stretch and the shrink when a project opens. */
.work_slot {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--work_thumb_w);
  height: 100%;
}

.work_card {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--project_colour);
  color: var(--charcoal);
  font-family: var(--font);
  text-align: left;
  cursor: inherit;
  scale: var(--work_stretch, 1) 1;   /* stretches along the row while it moves fast */
  -webkit-tap-highlight-color: transparent;
}

.work_card:focus-visible {
  outline: var(--focus_width) solid var(--charcoal);
  outline-offset: var(--focus_width);
}

/* The tab, in the project colour, with the name in small capitals. */
.work_tab {
  display: flex;
  align-items: center;
  flex: none;
  height: var(--work_tab_h);
  padding: 0 var(--work_tab_pad);
  background: var(--project_colour);
  font-size: var(--work_tab_text);
  font-weight: var(--weight_medium);
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
}

.work_name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The cover, cropped to fill 4:5. Until there is one, a darker tone of the project colour. */
.work_image {
  position: relative;
  display: block;
  flex: none;
  width: var(--work_image_w);
  aspect-ratio: var(--work_thumb_ratio);
  margin: 0 var(--work_card_border) var(--work_card_border);
  overflow: hidden;
  background: color-mix(in srgb, var(--project_colour), var(--charcoal) var(--project_cover_shade));
}

.work_image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--cover_focus, 50% 50%);   /* each project's focal point, from scripts/projects.js */
  pointer-events: none;
}

/* Waiting for the deal, every thumbnail is hidden; during it, the tabs are, fading in once
   the thumbnails reach their places. */
.work_row.is_waiting .work_card { opacity: 0; }

.work_tab { transition: opacity var(--fade_duration) var(--ease); }
.work_row.is_dealing .work_tab { opacity: 0; transition: none; }

/* ---------- An open project ----------
   Covers the whole screen, the rail and wordmark included. scripts/project_open.js grows
   the colour out of the thumbnail's tab, flies the image into place, decodes the name and
   fades in the rest. Laid out on a 12 column grid on laptops and tablets, with margins and
   gutters matching the hero border: the image fills columns 7 to 12 from the top margin to
   the bottom margin, the name sits bottom left across columns 1 to 6, the description under
   it, and the year and category run along the bottom margin under that, in two columns
   starting at 1 and 4. Any further sections follow below, and the page scrolls. */

.project_view {
  position: fixed;
  inset: 0;
  z-index: var(--layer_project);
  color: var(--charcoal);
  font-family: var(--font);
  container-type: size;   /* the name is sized from the screen */
}

.project_view[hidden] { display: none; }

/* While a project closes, nothing on it takes a click or a scroll. */
.project_view.is_closing { pointer-events: none; }

.project_view_colour {
  position: absolute;
  inset: 0;
  background: var(--project_colour);
}

.project_view_page {
  /* the width the name fills: its columns, with the gutters between them */
  --name_width: calc(
    (100cqi - var(--grid_margin) * 2 - var(--grid_gutter) * (var(--project_view_columns) - 1))
    / var(--project_view_columns) * var(--project_name_columns)
    + var(--grid_gutter) * (var(--project_name_columns) - 1)
  );
  --name_size: calc(var(--name_width) / var(--project_title_measure));

  position: absolute;
  inset: 0;
  padding: var(--grid_margin);
  overflow-x: hidden;
  overflow-y: auto;   /* a project with further sections scrolls; the close button stays put */
  overscroll-behavior: contain;
}

/* The first screen: the cover, the name, the description and the year and category. */
.project_view_first {
  display: grid;
  grid-template-columns: repeat(var(--project_view_columns), minmax(0, 1fr));
  grid-template-rows: 1fr auto auto auto;
  column-gap: var(--grid_gutter);
  min-height: calc(100cqh - var(--grid_margin) * 2);
}

/* The cover, exactly the thumbnail scaled up: the same 4:5 shape and the same focal point,
   so it crops the same. As large as fits columns 7 to 12 from the top border margin, the
   close button sitting over its corner, against the right margin. Until there is a cover, a darker tone of the project colour. */
.project_view_image {
  position: relative;
  grid-column: var(--project_image_column) / -1;
  grid-row: 1 / -1;
  justify-self: end;
  align-self: start;
  width: min(100%, (100cqh - var(--grid_margin) * 2 - var(--project_close_clear)) * var(--work_thumb_ratio));
  aspect-ratio: var(--work_thumb_ratio);
  margin-top: var(--project_close_clear);
  overflow: hidden;
  background: color-mix(in srgb, var(--project_colour), var(--charcoal) var(--project_cover_shade));
}

.project_view_image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--cover_focus, 50% 50%);
}

.project_view_image img[hidden] { display: none; }

/* Project covers take the paper grain but not the wash, so they keep their full depth and
   the space waiting for a cover stays darker than the project colour. */
.project_view_image::before,
.work_image::before { display: none; }

/* The name's look comes from the shared .title_type (base.css): Satoshi Black in
   capitals with the hero type's line and letter spacing, at every size. Every name
   is the same size: the widest line of the longest name fills its columns. */
.project_view_name {
  position: relative;
  grid-column: 1 / span var(--project_name_columns);
  grid-row: 2;
  margin: 0 0 var(--project_name_gap);
  font-size: var(--name_size);
}

/* The name and its year and category: laid straight onto the first screen's grid, except
   on a pinned page (below), where they move together. */
.project_view_label { display: contents; }

.project_view_details {
  display: grid;
  grid-template-columns: subgrid;
  grid-column: 1 / span var(--project_name_columns);
  grid-row: 4;
  font-size: var(--project_detail_size);
  font-weight: var(--weight_medium);
  line-height: var(--project_detail_leading);
  text-transform: uppercase;
}

.project_view_details p { margin: 0; }

/* The description: the first block of text, under the name, as a lead paragraph. On
   a laptop or tablet it only shows once written; phones show a placeholder until then.
   The further images (images in projects.js) show on phones only for now. */
.project_view_description {
  grid-column: 1 / span var(--project_name_columns);
  grid-row: 3;
  margin: 0 0 var(--project_description_gap);
  font-size: var(--text_lead);
  line-height: var(--line_lead);
}

.project_view_description p { margin: 0; }
.project_view_description p + p { margin-top: var(--project_paragraph_gap); }

.project_view_description.placeholder_text,
.project_view_gallery { display: none; }

.project_view_gallery img {
  display: block;
  width: 100%;
  height: auto;
}
.project_view_details span { display: block; }
.project_view_year { grid-column: 1 / var(--project_details_second); }
.project_view_category { grid-column: var(--project_details_second) / -1; }

/* Further sections, below the first screen: a full width image, or text on the left and
   an image on the right with their tops aligned, on the same grid as the first screen.
   Images keep their own shape. On a phone they stack, text first. */
.project_view_sections {
  display: flex;
  flex-direction: column;
  gap: var(--project_section_space);
}

.project_view_sections:not(:empty) { margin-top: var(--project_section_space); }

.project_section_text_image,
.project_section_text_image_mirrored {
  --split_gap: var(--project_section_stack);
  display: grid;
  grid-template-columns: repeat(var(--project_view_columns), minmax(0, 1fr));
  column-gap: var(--grid_gutter);
  row-gap: var(--split_gap);
  align-items: start;
}

.project_section_text {
  grid-column: 1 / span var(--project_name_columns);
  margin: 0;
  font-size: var(--text_lead);   /* the same lead size as the description */
  line-height: var(--line_lead);
}

.project_section_text_image .project_section_image { grid-column: var(--project_image_column) / -1; }

/* A text and image section's copy, when it has a second labelled block (then): the
   first text, then the label just under it, then the second text, in one column. */
.project_section_copy {
  grid-column: 1 / span var(--project_name_columns);
  display: flex;
  flex-direction: column;
}

.project_section_copy .project_section_label { margin-top: var(--section_then_space); }
.project_section_copy.is_spread { gap: var(--project_paragraph_gap); }

/* An extra image in the text's column, under its paragraphs, with its caption. */
.project_section_copy .project_section_image.is_extra { margin-top: var(--pinned_text_gap); }
.is_pinned .project_section_copy .project_section_image.is_extra,
.is_pinned .project_section_copy .project_section_caption.is_extra { width: 100%; }
.is_pinned .project_section_copy .project_section_caption.is_extra { margin-top: var(--caption_space); }

/* G, text on its own across the full width. */
.project_section_text_block .project_section_text { width: 100%; }

/* A text block reads a little larger and heavier than the other large text. */
.is_pinned .project_section_text_block .project_section_text {
  font-size: calc(max(var(--pinned_lead_min), calc(var(--name_size) * var(--pinned_lead_scale))) * var(--text_block_scale));
  font-weight: var(--text_block_weight);
}

/* Paragraphs set editorially: closer together, each three quarters of the column wide,
   alternating left and right. */
.project_section_copy.is_staggered { gap: var(--stagger_text_gap); }
.project_section_copy.is_staggered .project_section_text {
  width: calc(100% * max(var(--stagger_text_min), var(--stagger_share, var(--stagger_text_width))));
  text-wrap: pretty;   /* an even right edge, no single word left on the last line */
}
.project_section_copy.is_staggered .project_section_text:nth-child(even) { align-self: flex-end; }

/* Beside a filled image in the left zone, the left hand paragraphs reach part of the way
   into the space between the image and the column. */
@media (min-width: 768px) {
  .is_pinned .project_section_text_image_mirrored.is_fill .is_staggered .project_section_text:nth-child(odd) {
    margin-left: calc(-1 * var(--stagger_pull) * (100cqi * var(--pinned_split) - var(--grid_margin) - var(--pinned_column)));
  }
}
.project_section_copy .project_section_label + .project_section_text { margin-top: var(--section_label_space); }
.project_section_text_image_mirrored .project_section_image,
.project_section_text_image_mirrored .project_section_caption { grid-column: 1 / span var(--project_name_columns); }
.project_section_text_image_mirrored .project_section_text { grid-column: var(--project_image_column) / -1; }

/* A label sits close above its text: the text cancels most of the grid gap. */
.project_section_label + .project_section_text { margin-top: calc(var(--section_label_space) - var(--split_gap)); }

/* An image's caption sits close under it, not a grid gap away. */
.project_section_text_image .project_section_caption,
.project_section_text_image_mirrored .project_section_caption { margin-top: calc(var(--caption_space) - var(--split_gap)); }

.project_section_image,
.project_section_image img {
  display: block;
  width: 100%;
  height: auto;
}

/* A billboard can be set smaller than the full width, against the left margin. */
.project_section_billboard { width: calc(100% * var(--billboard_scale, 1)); }

/* A caption under a section image, in the label style: its number, a wide gap, then its
   words, on one line. */
.project_section_caption {
  display: flex;
  margin: var(--caption_space) 0 0;
  font-size: var(--project_detail_size);
  font-weight: var(--weight_medium);
  line-height: var(--project_detail_leading);
  text-transform: uppercase;
}

.project_caption_number { flex: none; width: var(--caption_number_width); }

/* A section video: the billboard's exact shape, the film cropped to fill it. */
.project_section_video {
  aspect-ratio: var(--billboard_ratio);
  overflow: hidden;
  background: var(--charcoal);
}

.project_section_video video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project_section_video.reveal { transform: translateY(var(--reveal_rise)); }

/* A video keeping its own shape: in a pair, or a video billboard with fit: 'own'. Never
   taller than the page; if it would be, its width follows the capped height. */
.project_section_pair .project_section_video,
.project_section_video_billboard.is_own_shape .project_section_video {
  aspect-ratio: var(--video_aspect);
  width: min(100%, (100cqh - var(--grid_margin) * 2) * var(--video_aspect));
}

/* A mockup pair: a landscape image on the left from the left border margin, a little
   narrower than the pinned page's left zone, with its caption; and a portrait image
   against the right border margin, its top level with the left one's. The portrait one is
   scaled up from a width of --pair_right_start of the page until it reaches as high as the
   left image's top would be from its old place halfway down, its bottom staying put. The
   space between stays the page colour. */
.project_section_pair {
  --pair_left_w: calc((100cqi * var(--pinned_split) - var(--grid_margin)) * var(--pair_left_scale));
  --pair_right_base: calc(100cqi * (1 - var(--pair_right_start)) - var(--grid_margin));
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
}

.project_pair_left { width: var(--pair_left_w); }

.project_pair_right {
  width: calc(var(--pair_right_base) + var(--pair_left_w) * var(--pair_left_ratio) / 2 / var(--pair_right_ratio));
}

.project_pair_right .reveal { transition-delay: var(--reveal_stagger); }

/* D with two portraits: the left one a share of the left zone from the left margin, the
   right one filling the right column, in line with the first screen's column, starting
   --pair_portrait_stagger of the way down the left one. */
.project_section_pair.is_portraits {
  --pair_left_w: calc((100cqi * var(--pinned_split) - var(--grid_margin)) * var(--pair_portrait_left_scale));
}

.project_section_pair.is_portraits .project_pair_right {
  width: var(--pinned_column);
  margin-top: calc(min(var(--pair_left_w) * var(--pair_left_ratio), 100cqh - var(--grid_margin) * 2) * var(--pair_portrait_stagger));
}

/* F, series: tall images side by side at one height, each taking width in proportion to
   its shape, revealed one after another. Rows of four on a tablet; on a phone one row
   that scrolls sideways. */
.project_series_row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--series_gap);
}

.project_series_row > .project_section_image,
.project_series_row > .project_series_item {
  flex: var(--series_aspect) 1 0;
  min-width: 0;
}

.project_series_row .project_section_image.reveal,
.project_series_item .project_section_caption.reveal {
  transition-delay: calc(var(--series_index) * var(--series_stagger));
}

.project_series_break { display: none; }

@media (min-width: 600px) and (max-width: 1024px) {
  .project_series_break { display: block; flex-basis: 100%; height: 0; }
}

@media (min-width: 600px) {
  /* In rows, a series spans the full width in rows of four, the same gap across and down. */
  .project_series_row.is_rows {
    display: grid;
    grid-template-columns: repeat(var(--series_rows_count), minmax(0, 1fr));
    gap: var(--series_rows_gap);
  }

  .project_series_row.is_rows .project_series_break { display: none; }
}

/* A text and image section with fill: true: its image fills the column's width, however
   tall that makes it. */
.project_section.is_fill .project_section_image img {
  width: 100%;
  max-height: none;
}

/* A tall image in a pair never runs taller than the screen; its width follows. */
.project_section_pair .project_section_image img {
  width: auto;
  max-width: 100%;
  max-height: calc(100cqh - var(--grid_margin) * 2);
}

/* A tall image beside or below text never runs taller than the screen. */
.project_section_text_image .project_section_image img,
.project_section_text_image_mirrored .project_section_image img {
  width: auto;
  max-width: 100%;
  max-height: calc(100cqh - var(--grid_margin) * 2);
}

/* A small label above a section's text, in the label style of the year and category. */
.project_section_label {
  grid-column: 1 / -1;
  margin: 0;
  font-size: calc(var(--project_detail_size) * var(--section_label_scale));
  font-weight: var(--weight_medium);
  line-height: var(--project_detail_leading);
  text-transform: uppercase;
}

/* Large text revealed line by line (scripts/line_reveal.js): each line slides up from
   behind a mask one line high. The mask reaches a little below the line so descenders
   are never clipped. */
.line_mask {
  display: block;
  overflow: hidden;
  padding-bottom: var(--line_mask_bleed);
  margin-bottom: calc(var(--line_mask_bleed) * -1);
}

.line_inner {
  display: block;
  white-space: nowrap;
  transform: translateY(var(--line_reveal_from));
  transition: transform var(--line_reveal_duration) var(--ease) var(--line_delay, 0ms);
}

.line_mask.is_revealed .line_inner { transform: none; }
.line_mask.no_transition .line_inner { transition: none; }

/* ---------- A pinned project page (Tastebud) ----------
   Laptops and tablets: two zones. The right column starts at --pinned_split of the page
   width and runs to the right border margin. On load the cover fills its width from just
   under the close button, at the thumbnail's 4:5 shape; the description follows
   directly beneath it, large. Meanwhile the name and its labels stay pinned bottom left
   (a sticky element inside the tall first section, so normal scrolling is never taken
   over) until the description's last line reaches them, and then scroll away with it.
   After a generous gap the billboard, then a small label and the second text across the
   full width, and the square image below it in the right column. */
.project_view {
  --pinned_column: calc(100cqi * (1 - var(--pinned_split)) - var(--grid_margin));
  --left_zone: calc(100cqi * var(--pinned_split) - var(--grid_margin) - var(--line_gutter));   /* lines 1 to 2, on a project with align: 'lines' */
  --tall_cap: calc(100cqh - var(--grid_margin) * 2);   /* there, the tallest an image or video gets before it is cropped */
}

.is_pinned .project_view_description,
.is_pinned .project_section_text {
  font-size: max(var(--pinned_lead_min), calc(var(--name_size) * var(--pinned_lead_scale)));
  font-weight: var(--pinned_lead_weight);
  line-height: var(--pinned_lead_leading);
  letter-spacing: var(--pinned_lead_tracking);
}

@media (min-width: 768px) {
  .is_pinned .project_view_first {
    grid-template-columns: minmax(0, 1fr) var(--pinned_column);
    grid-template-rows: auto auto;
    column-gap: 0;
    min-height: 0;
  }

  /* The cover keeps its thumbnail's exact 4:5 shape, so opening a project is a single
     move and scale with no change of crop. It is always the right column's full width,
     the same as the text under it; on a short screen it runs on below the fold. */
  .is_pinned .project_view_image {
    grid-column: 2;
    grid-row: 1;
    justify-self: end;
    width: min(100%, (100cqh - var(--grid_margin) * 2) * var(--work_thumb_ratio));   /* the column's width, unless the whole cover would not fit the first screen */
    aspect-ratio: var(--work_thumb_ratio);
  }

  .is_pinned .project_view_description {
    grid-column: 2;
    grid-row: 2;
    margin: var(--pinned_text_gap) 0 0 var(--lead_optical_shift);   /* same column start as the cover, nudged for the first capital's side bearing */
  }

  .is_pinned .project_view_label {
    display: block;
    grid-column: 1;
    grid-row: 1 / -1;
    align-self: end;
    position: sticky;
    bottom: 0;   /* the page's own padding already keeps it inside the border margin */
    padding-right: var(--grid_gutter);
  }

  .is_pinned .project_view_details {
    display: flex;
    flex-direction: column;
    gap: var(--project_details_stack);
  }

  .is_pinned .project_view_sections {
    gap: 0;
    margin-top: var(--pinned_billboard_gap);
  }

  .is_pinned.is_intro_close .project_view_sections { margin-top: var(--pinned_intro_close_gap); }

  /* Modules on the pinned page's two zones: the left zone and the right column. Each
     section after the first sits --pinned_lower_gap below the one before; a text and
     image section a larger --pinned_section_gap. */
  .is_pinned .project_section + .project_section { margin-top: var(--pinned_lower_gap); }

  /* A tucked pair rises past the caption of the image above it, so its left image
     starts --pair_tuck above that image's bottom edge. */
  .is_pinned .project_section.project_section_pair.is_tucked {
    margin-top: calc(-1 * (var(--caption_space) + var(--project_detail_size) * var(--project_detail_leading) + var(--pair_tuck)));
  }

  /* C, text and image: the text in the left zone and the image in the right column,
     tops aligned, the caption under the image. Mirrored: the image in the left zone and
     the text in the right column, in line with the first screen's column. */
  .is_pinned .project_section.project_section_text_image,
  .is_pinned .project_section.project_section_text_image_mirrored {
    --split_gap: var(--caption_space);
    grid-template-columns: minmax(0, 1fr) var(--pinned_column);
    column-gap: 0;
    margin-top: var(--pinned_section_gap);
  }

  .is_pinned .project_section_text_image .project_section_text {
    grid-column: 1;
    grid-row: 1;
    padding-right: var(--grid_gutter);
  }

  .is_pinned .project_section_text_image .project_section_copy { grid-column: 1; grid-row: 1; }

  /* Mirrored, the copy sits in the right column beside the image in the left zone. With
     fill, the image there is the right column's width, as it would be on the right. */
  .is_pinned .project_section_text_image_mirrored .project_section_copy { grid-column: 2; grid-row: 1; }
  .is_pinned .project_section_text_image_mirrored.is_fill .project_section_image img { width: var(--pinned_column); }

  /* Several paragraphs beside a tall image, spread evenly from its top to its bottom. */
  .is_pinned .project_section_text_image .project_section_copy.is_spread {
    align-self: stretch;
    justify-content: space-between;
    padding-right: var(--grid_gutter);
  }

  .is_pinned .project_section_text_image .project_section_image,
  .is_pinned .project_section_text_image .project_section_caption { grid-column: 2; }
  .is_pinned .project_section_text_image .project_section_image { grid-row: 1; }

  .is_pinned .project_section_text_image_mirrored .project_section_text { grid-column: 2; grid-row: 1; }

  /* With a label (like Tastebud's DESIGN CHALLENGE:), the label sits above the text and
     the image's top lines up with the label's. */
  .is_pinned .project_section_text_image:not(.is_wide):has(.project_section_label) { grid-template-rows: auto 1fr auto; }
  .is_pinned .project_section_text_image:not(.is_wide):has(.project_section_label) .project_section_label { grid-column: 1; grid-row: 1; }
  .is_pinned .project_section_text_image:not(.is_wide):has(.project_section_label) .project_section_text { grid-row: 2; }
  .is_pinned .project_section_text_image:not(.is_wide):has(.project_section_label) .project_section_image { grid-row: 1 / span 2; }
  .is_pinned .project_section_text_image:not(.is_wide):has(.project_section_label) .project_section_caption { grid-row: 3; }

  .is_pinned .project_section_text_image_mirrored .project_section_image,
  .is_pinned .project_section_text_image_mirrored .project_section_caption {
    grid-column: 1;
    width: calc(100% - var(--grid_gutter));
  }
  .is_pinned .project_section_text_image_mirrored .project_section_image { grid-row: 1; }

  /* C, wide (Tastebud): the label and text run the full width straight under the
     billboard; the image sits below them in the right column, in line with the cover.
     The label and text sit a little higher; the image below stays where it was. */
  .is_pinned .project_section.project_section_text_image.is_wide {
    --split_gap: var(--pinned_image_gap);   /* the text to the image below it */
    margin-top: calc(var(--pinned_section_gap) - var(--pinned_challenge_lift));
  }

  .is_pinned .project_section_text_image.is_wide .project_section_text {
    grid-column: 1 / -1;
    grid-row: auto;
    padding-right: 0;
  }

  .is_pinned .project_section_text_image.is_wide .project_section_image {
    grid-row: auto;
    margin-top: var(--pinned_challenge_lift);
  }

  /* A section marked closer sits nearer the one before it. */
  .is_pinned .project_section.is_closer { margin-top: var(--pinned_close_gap); }

  /* A section marked tight sits just under the one before it. */
  .is_pinned .project_section.is_tight { margin-top: var(--pinned_text_gap); }

  /* A text and image section with drop: true has its image sitting lower than its text. */
  .is_pinned .project_section.is_dropped .project_section_image { margin-top: var(--image_drop); }

  /* A text and image section with lift: true has its image rising into the gap above. */
  .is_pinned .project_section.is_lifted .project_section_image { margin-top: calc(-1 * var(--image_lift)); }
  .is_pinned .project_section.is_lifted_more .project_section_image { margin-top: calc(-1 * var(--image_lift_more)); }

  /* A text and image section with textDown: true has its text sitting lower. */
  .is_pinned .project_section.is_text_down .project_section_text { margin-top: var(--image_drop); }

  /* Before a closer section, a pair's taller right image hangs down beside it rather than
     pushing it down: the gap is measured from the left image's caption. */
  .is_pinned .project_section_pair:not(.is_portraits):has(+ .is_closer) .project_pair_right {
    margin-bottom: calc(-1 * (
      (var(--pair_right_base) + var(--pair_left_w) * var(--pair_left_ratio) / 2 / var(--pair_right_ratio)) * var(--pair_right_ratio)
      - var(--pair_left_w) * var(--pair_left_ratio)
      - var(--caption_space) - var(--project_detail_size) * var(--project_detail_leading)
    ));
  }
}

/* Laptops: with fill: 'name', the image is as wide as the project name's widest line. */
@media (min-width: 1025px) {
  .is_pinned .project_section.is_fill_name .project_section_image img { width: calc(var(--name_size) * var(--project_title_line)); }
}

/* ---------- The alignment lines (align: 'lines', Pae and Homesick) ----------
   Only a project that sets align: 'lines' in scripts/projects.js gets these; every other
   project keeps the rules above exactly. On tablets and laptops everything snaps to four
   vertical lines: 1 the left margin, 2 the end of the left zone (the split minus one
   gutter), 3 the split, 4 the right margin. Widths come from the lines, heights from each
   item's own shape, and a caption is exactly as wide as its media. A portrait or video
   taller than the screen keeps its width and is cropped to the screen's height around its
   focus point. Spread and split are built for these lines. */

/* Spread: two columns that stack independently, like a magazine spread. Left blocks fill
   the left zone and right blocks the right column, each column stacking its blocks
   --stack_gap apart and starting its offset lower. On a phone, one column in reading
   order (each block carries its place in the list as its order). */
.project_section_spread {
  display: flex;
  flex-direction: column;
  gap: var(--stack_gap);
}

.project_spread_column { display: contents; }
.project_spread_block { order: var(--block_order); }
.project_spread_block .project_section_label + .project_section_text { margin-top: var(--section_label_space); }

/* Split: two media side by side at one height, the right one's natural height in the
   right column; the left one is cropped to it around its focus point. On a phone they
   stack, each at its own shape. */
.project_section_split {
  display: flex;
  flex-direction: column;
  gap: var(--stack_gap);
}

.project_split_right .reveal { transition-delay: var(--reveal_stagger); }

/* A photo mockup still to come: a dashed outline at its shape, labelled. */
.project_section_slot {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: var(--slot_ratio);
  border: var(--slot_border) dashed currentColor;
}

.project_slot_label {
  font-size: var(--project_detail_size);
  font-weight: var(--weight_medium);
  line-height: var(--project_detail_leading);
  text-transform: uppercase;
  opacity: var(--slot_opacity);
}

.project_section_slot.reveal.is_revealed { opacity: 1; }

/* Paragraphs in a spread block, a paragraph's space apart. */
.project_spread_block .project_section_text + .project_section_text { margin-top: var(--project_paragraph_gap); }

/* J, logo reveal: the logo on a panel of the brand's colour at the billboard's shape. */
.project_logo_reveal {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: var(--billboard_ratio);
  background: var(--logo_panel);
}

.project_logo_reveal svg {
  display: block;
  width: var(--logo_reveal_width);
  height: auto;
  overflow: visible;
}

.project_logo_reveal svg g { fill: var(--logo_ink); }

.project_logo_mark {
  stroke: var(--logo_ink);
  stroke-width: var(--logo_stroke);
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  fill-opacity: 0;
  transition:
    stroke-dashoffset var(--logo_draw_duration) var(--ease_in_out),
    fill-opacity var(--logo_fill_duration) var(--ease_in_out) var(--logo_draw_duration);
}

.project_logo_letter {
  opacity: 0;
  transform: translateY(var(--logo_letter_rise));
  transform-box: fill-box;
  transition:
    opacity var(--logo_letter_duration) var(--ease_in_out),
    transform var(--logo_letter_duration) var(--ease_in_out);
  transition-delay: calc(var(--logo_draw_duration) + var(--letter_index) * var(--logo_letter_stagger));
}

.project_logo_reveal.is_revealed .project_logo_mark { stroke-dashoffset: 0; fill-opacity: 1; }
.project_logo_reveal.is_revealed .project_logo_letter { opacity: 1; transform: none; }

/* Videos in a spread or split keep their own shape across their column. */
.project_section_spread .project_section_video,
.project_section_split .project_section_video {
  aspect-ratio: var(--video_aspect, var(--billboard_ratio));
  width: 100%;
  max-height: var(--tall_cap);
}

.project_section_video video {
  object-position: var(--focus, 50% 50%);
  transform: scale(var(--video_trim, 1));   /* trim: a border baked into the footage pushed out of view */
}

/* A series on the lines: one gutter between its images, across and down. */
.is_aligned .project_series_row,
.is_aligned .project_series_row.is_rows { gap: var(--series_lines_gap); }

@media (min-width: 768px) {
  .project_section_spread,
  .project_section_split {
    display: grid;
    grid-template-columns: var(--left_zone) var(--pinned_column);
    column-gap: var(--line_gutter);
    row-gap: 0;   /* a wide block above the columns carries its own space */
    align-items: start;
  }

  .project_spread_column {
    display: flex;
    flex-direction: column;
    gap: var(--stack_gap);
    grid-row: 2;
  }

  /* A wide block runs the full width above both columns, like Tastebud's wide design
     challenge, with the same space under it as there. */
  .project_spread_block.is_wide {
    grid-column: 1 / -1;
    grid-row: 1;
    margin-bottom: var(--pinned_image_gap);
  }

  /* A spread holding only wide text keeps no space under it: the next section's own gap follows. */
  .project_section_spread:not(:has(.project_spread_column > *)) .project_spread_block.is_wide { margin-bottom: 0; }

  /* A scaled block is that share of its column, against the column's outer edge: the
     right margin in the right column, the left margin in the left zone. */
  .project_spread_block.is_scaled {
    width: calc(100% * var(--block_scale));
    align-self: flex-end;
  }

  .project_spread_left .project_spread_block.is_scaled { align-self: flex-start; }

  /* A block the right column's exact width, against its column's outer edge. */
  .project_spread_block.is_column_width { width: var(--pinned_column); }
  .project_spread_right .project_spread_block.is_column_width { align-self: flex-end; }

  /* A cleared block starts below both columns, in its own column. */
  .project_spread_block.is_cleared { grid-row: 3; }
  .project_spread_block.is_cleared.is_left { grid-column: 1; }
  .project_spread_block.is_cleared.is_right { grid-column: 2; }

  /* A block the left zone's width. Cleared in the right column, it spans both columns
     and keeps its right edge on the right margin. */
  .project_spread_block.is_zone_width { width: var(--left_zone); }

  /* A block set to a width in pixels on a 1440 laptop, scaling with the page, reaching
     in from its column's outer edge. */
  .project_spread_block.is_set_width {
    width: calc(100cqi * var(--block_width) / var(--layout_reference));
    align-self: flex-end;
  }
  .project_spread_left .project_spread_block.is_set_width { align-self: flex-start; }
  .project_spread_block.is_cleared.is_right.is_zone_width {
    grid-column: 1 / -1;
    justify-self: end;
  }

  /* Videos in a spread show no frame colour round them. */
  .is_aligned .project_section_spread .project_section_video { background: none; }

  /* Extra space above a block, on top of the stack gap. */
  .project_spread_block { margin-top: calc(100cqi * var(--block_gap_before, 0) / var(--layout_reference)); }

  /* A split's right one scaled at its own shape, against the right margin, and with
     alignBottom its bottom level with the left one's. */
  .project_section_split.is_right_scaled .project_split_right {
    width: calc(var(--pinned_column) * var(--split_right_scale));   /* from the column itself: Safari reads a percentage here as zero */
    justify-self: end;
  }

  .project_section_split.is_right_scaled .project_split_right .project_section_image img {
    height: auto;
    object-fit: fill;
  }

  .project_section_split.is_bottom_aligned .project_split_right { align-self: end; }

  .project_spread_left { grid-column: 1; padding-top: calc(100cqi * var(--spread_offset_left, 0) / var(--layout_reference)); }
  .project_spread_right { grid-column: 2; padding-top: calc(100cqi * var(--spread_offset_right, 0) / var(--layout_reference)); }

  .project_section_split {
    --split_height: min(var(--pinned_column) * var(--split_ratio), var(--tall_cap));
  }

  .project_section_split .project_section_image img,
  .project_section_split .project_section_video {
    height: var(--split_height);
    max-height: none;
    aspect-ratio: auto;
    object-fit: cover;
    object-position: var(--focus, 50% 50%);
  }

  /* Billboards at full width: no scale. */
  .is_aligned .project_section_billboard { width: 100%; }

  /* Every image fills its column; only a tall portrait is cropped, never a series or a
     section with fill. */
  .is_aligned.is_pinned .project_section .project_section_image img {
    width: 100%;
    max-width: none;
    max-height: none;
  }

  .is_aligned.is_pinned .project_section .project_section_image.is_portrait img {
    max-height: var(--tall_cap);
    object-fit: cover;
    object-position: var(--focus, 50% 50%);
  }

  .is_aligned.is_pinned .project_series_row .project_section_image.is_portrait img,
  .is_aligned.is_pinned .project_section.is_fill .project_section_image.is_portrait img { max-height: none; }

  .is_aligned.is_pinned .project_section_split .project_section_image.is_portrait img { max-height: none; }

  /* Videos keeping their own shape fill their column, cropped if taller than the screen. */
  .is_aligned .project_section_pair .project_section_video,
  .is_aligned .project_section_video_billboard.is_own_shape .project_section_video {
    width: 100%;
    max-height: var(--tall_cap);
  }

  /* Pair: the left one fills the left zone, the right one the right column. */
  .is_aligned .project_pair_left { width: var(--left_zone); }
  .is_aligned .project_pair_right,
  .is_aligned .project_section_pair.is_portraits .project_pair_right { width: var(--pinned_column); }
  .is_aligned .project_section_pair.is_portraits .project_pair_right {
    margin-top: calc(min(var(--left_zone) * var(--pair_left_ratio), var(--tall_cap)) * var(--pair_portrait_stagger));
  }

  .is_aligned.is_pinned .project_section_pair:not(.is_portraits):has(+ .is_closer) .project_pair_right {
    margin-bottom: calc(-1 * (
      min(var(--pinned_column) * var(--pair_right_ratio), var(--tall_cap))
      - var(--left_zone) * var(--pair_left_ratio)
      - var(--caption_space) - var(--project_detail_size) * var(--project_detail_leading)
    ));
  }

  /* Text and image: the left zone and the right column, one gutter between. */
  .is_aligned.is_pinned .project_section.project_section_text_image,
  .is_aligned.is_pinned .project_section.project_section_text_image_mirrored {
    grid-template-columns: var(--left_zone) var(--pinned_column);
    column-gap: var(--line_gutter);
  }

  .is_aligned.is_pinned .project_section_text_image .project_section_text,
  .is_aligned.is_pinned .project_section_text_image .project_section_copy.is_spread { padding-right: 0; }

  .is_aligned.is_pinned .project_section_text_image_mirrored .project_section_image,
  .is_aligned.is_pinned .project_section_text_image_mirrored .project_section_caption { width: 100%; }
}

/* Each part is revealed once as its section scrolls into view (scripts/project_open.js):
   text fades in, an image rises a short way as it fades in, a beat after the text. */
.reveal {
  opacity: 0;
  transition:
    opacity var(--reveal_duration) var(--ease),
    transform var(--reveal_duration) var(--ease);
}

.project_section_image.reveal { transform: translateY(var(--reveal_rise)); }
.project_section_text_image .project_section_image.reveal { transition-delay: var(--reveal_stagger); }

/* Mirrored, the image on the left and its text come in together. */
.project_section_text_image_mirrored .project_section_image.reveal { transition-delay: 0ms; }

.reveal.is_revealed {
  opacity: 1;
  transform: none;
}

/* The close button: an X of two thin lines in charcoal, on a circle of the project colour.
   It sits in the top right corner, just above the image, so it never covers a logo. */
.project_view_close {
  position: absolute;
  top: var(--grid_margin);
  right: var(--grid_margin);
  width: var(--project_close_size);
  height: var(--project_close_size);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--project_colour);   /* a solid backing in the page colour, so it reads over any image */
  color: var(--charcoal);
  cursor: pointer;
  z-index: var(--project_close_layer);
}

.project_view_close::before,
.project_view_close::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--project_close_cross);
  height: var(--project_close_line);
  background: currentColor;
  transform: translate(-50%, -50%) rotate(45deg);
}

.project_view_close::after { transform: translate(-50%, -50%) rotate(-45deg); }

.project_view_close:focus-visible {
  outline: var(--focus_width) solid currentColor;
  outline-offset: var(--focus_width);
}

/* ---------- About Me ----------
   On a laptop or tablet the photo sits on the left, as tall as the page inside the hero
   border margin, its width following its 4:5 shape, and it
   stays in view while the copy beside it scrolls. If the page is too narrow, the photo
   shrinks to leave the copy its narrowest column. The copy: a small spaced label, the
   top text set like a project name, then two justified body paragraphs (two columns on
   very wide screens), then the CV link as a small underlined label. On a phone it all
   stacks in reading order, the body left aligned. The photo fades in as the page
   finishes sliding open. */

.sheet_about .sheet_scroll {
  padding: 0;
  container-type: size;   /* the photo is sized from the open page */
}

.about {
  --about_pad_right: var(--hero_gap);
  display: flex;
  align-items: flex-start;
  gap: var(--hero_gap);
  min-height: 100%;
  padding: var(--hero_gap) var(--about_pad_right) var(--hero_gap) var(--hero_gap);
  color: var(--charcoal);
}

.about_figure {
  position: sticky;
  top: var(--hero_gap);
  flex: none;
  margin: 0;
  opacity: 0;
  transition: opacity var(--about_fade_duration) var(--ease) var(--about_fade_delay);
}

.sheet_about.is_shown .about_figure { opacity: 1; }

.about_photo {
  --about_photo_h: min(
    100cqh - var(--hero_gap) * 2,
    (100cqi - var(--hero_gap) * 2 - var(--about_pad_right) - var(--about_text_min)) * var(--about_photo_tall)
  );
  position: relative;
  display: block;
  height: var(--about_photo_h);
  /* the width set from the height and 4:5 shape, rather than left to aspect-ratio: Safari
     otherwise sizes the photo's figure to the image file's own width and pushes the text
     off the page */
  width: calc(var(--about_photo_h) * var(--about_photo_ratio));
  aspect-ratio: var(--about_photo_ratio);
  overflow: hidden;
}

.about_photo img {
  width: 100%;
  height: 100%;
}

.about_text {
  flex: 1;
  min-width: 0;
  container: about_text / inline-size;
  text-align: left;
  hyphens: none;
}

:where(.about_text p) { margin: 0; }   /* no weight, so each part's own spacing below wins */

/* The CV link, as a small spaced label. */
.about_cv .text_link {
  font-size: var(--text_small);
  font-weight: var(--weight_medium);
  line-height: 1;
  letter-spacing: var(--about_label_tracking);
  text-transform: uppercase;
}

/* The top text: the project name's look (.title_type), as large as it can be while its
   longest word (COMMUNICATION) still fits the column. On a laptop or tablet
   scripts/about.js makes it smaller if needed (--about_title_fit), so the whole copy
   column fits on one screen. */
.about_title {
  max-width: calc(100% * var(--about_title_share));
  line-height: var(--about_title_leading);   /* a touch looser than a project name, so commas clear the capitals below */
  font-size: var(--about_title_fit, calc(100cqi * var(--about_title_share) / var(--about_title_measure)));
}

/* Its three thoughts each start a new line, a small gap apart. Normally they wrap so the
   right edge stays even and no line ends on a single word. */
.about_title p { text-wrap: pretty; }
.about_title p + p { margin-top: var(--about_title_gap); }

/* Set (laptops and tablets, when the column allows): Harvey's own line breaks, each line
   stretched to the box's right edge by scripts/about.js adjusting its letter spacing,
   except the last line of each thought. The trailing letter spacing after each line's
   last letter is clipped so it never adds width. */
.about_title.is_set {
  width: var(--about_title_set_width);
  max-width: none;
  overflow-x: clip;
}

.about_title.is_set .about_line {
  display: block;
  white-space: nowrap;
}

/* The body: the site's body text, justified, with no word ever broken across lines. */
.about_body {
  max-width: var(--about_body_measure);
  margin-top: var(--about_section_space);
  font-size: var(--text_body);
  line-height: var(--line_body);
  text-align: justify;
}

.about_body p + p { margin-top: var(--about_paragraph_space); }

/* Laptops: the two body paragraphs sit side by side under the top text, spanning exactly
   its width so the two share a right edge. (On a tablet's narrower column they stack.) */
@media (min-width: 1025px) {
  .about_body {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--about_body_column_gap);
    max-width: calc(100% * var(--about_title_share));
  }

  .about_body p + p { margin-top: 0; }
}

/* When the top text is set with its own breaks, the body spans exactly its width. */
.about:has(.about_title.is_set) .about_body { max-width: var(--about_title_set_width); }

.about_cv { margin-top: var(--about_cv_space); }

/* ---------- MY WORK wordmark ----------
   Centred at the top of the page, at its widest spanning --wordmark_columns grid columns. The
   letters are laid out at a fixed size and the stage is scaled to that width (see
   scripts/wordmark.js). */

.wordmark {
  position: absolute;
  top: var(--grid_margin);
  left: 0;
  right: 0;
  margin: 0 auto;
  width: calc(var(--work_column) * var(--wordmark_columns) + var(--grid_gutter) * (var(--wordmark_columns) - 1));
  pointer-events: none;
}

.wordmark_stage {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
}

.wordmark_char {
  position: absolute;
  top: 0;
  left: 0;
  color: var(--wordmark_colour);
  font-family: var(--font);
  font-size: var(--wordmark_layout_size);
  font-weight: var(--weight_medium);
  line-height: 1;
  white-space: pre;
  will-change: transform;
}

.sheet_work { background: var(--brand_white); }
.sheet_about { background: var(--pale_yellow); }
.sheet_contact { background: var(--warm_brown); }

/* Opening straight onto a page from a link, or switching layout, with no slide. */
.rail.no_transition .rail_item { transition: none; }

/* ---------- Phone and tablet ----------
   The rail lies across the bottom. Opening a page moves the chosen banner and those
   above it to the top, dragging its colour up behind it. */

@media (max-width: 1024px) {
  /* The image runs under the banners to the bottom border, and the type stays clear
     of the part they cover. */
  .hero { padding-right: var(--hero_gap); }

  .hero_image {
    --type_clear_bottom: calc(var(--rail_width) - var(--hero_gap));
    --type_fit_h: calc(
      (100cqh - var(--type_clear_bottom))
      / (var(--hero_type_lines) * var(--hero_type_leading) + var(--type_pads))
    );
  }

  .rail_item {
    top: auto;
    right: 0;
    bottom: calc(var(--rail_strip) * (var(--rail_count) - 1 - var(--i)));
    left: 0;
    width: auto;
    height: var(--rail_strip);
  }

  .rail_item.is_moved { transform: translateY(calc(var(--rail_width) - 100cqh)); }

  .banner_inner {
    top: 0;
    width: 100cqw;
    height: 100cqh;
    transform: none;
  }

  .sheet {
    top: 100%;
    left: 0;
  }

  .sheet_scroll {
    right: 0;
    bottom: var(--rail_width);
  }

}

/* ---------- Phone only ---------- */

@media (max-width: 599px) {
  /* About Me: one column: photo, top text, the two body paragraphs side by side,
     CV link. The page scrolls. */
  .about { flex-direction: column; align-items: stretch; gap: 0; }
  .about_text { display: contents; }
  .about_figure { order: 1; position: static; margin-bottom: var(--about_section_space); }
  /* the photo full width, its height capped so the top text starts in the first screen,
     cropped around Harvey rather than squashed */
  .about_photo { width: 100%; height: auto; aspect-ratio: auto; }
  .about_photo img {
    height: auto;
    aspect-ratio: var(--about_photo_ratio);
    max-height: var(--about_photo_cap_phone);
    object-fit: cover;
    object-position: var(--about_photo_focus_phone);
  }
  /* the top text wraps normally on a phone: no hand set breaks or justification */
  .about_title { order: 2; max-width: none; font-size: var(--about_title_size_phone); }
  .about_body {
    order: 3;
    display: grid;
    grid-template-columns: 1fr 1fr;   /* the two paragraphs side by side, as on a laptop */
    column-gap: var(--about_body_column_gap_phone);
    text-align: left;                 /* justified text opens wide gaps in a narrow column */
    hyphens: none;
  }

  /* the two paragraphs rise and fade in as the page is scrolled a little */
  .about_body p {
    opacity: 0;
    transform: translateY(var(--reveal_rise));
    transition:
      opacity var(--reveal_duration) var(--ease),
      transform var(--reveal_duration) var(--ease);
  }
  .about_body p + p { transition-delay: var(--reveal_stagger); }
  .about_body p.is_revealed { opacity: 1; transform: none; }
  .about_body p + p { margin-top: 0; }
  .about_cv { order: 4; margin-bottom: var(--about_cv_space); }

  /* MY WORK: each thumbnail is a share of the screen width, so the next one peeks in. */
  .work {
    --work_thumb_w: calc(100cqi * var(--work_thumb_phone));
    --work_image_w: calc(var(--work_thumb_w) - var(--work_card_border) * 2);
  }
}

/* About Me on a laptop: the copy's column runs almost up to the MY WORK banner on the right. */
@media (min-width: 1025px) {
  .about { --about_pad_right: var(--about_banner_gap); }
}

/* ---------- Contact ----------
   Laid out like a project page's first screen: the photo in the right column from the top
   border margin to the bottom one, its width following its 3:4 shape (never wider than the
   project pages' right column); the three contacts bottom left, each a small label over a
   line set like a project name. scripts/contact.js sizes the lines together so the email
   fits on one line, and reveals them after the photo has faded in. On a phone the photo
   comes first at full width, then the lines. */

.sheet_contact .sheet_scroll {
  padding: 0;
  container-type: size;   /* sized from the open page */
}

.contact {
  --contact_photo_w: min((100cqh - var(--hero_gap) * 2) * var(--contact_photo_ratio), 100cqi * (1 - var(--pinned_split)) - var(--hero_gap));
  /* the project name's size, from the same grid */
  --contact_name_size: calc((
    (100cqi - var(--hero_gap) * 2 - var(--grid_gutter) * (var(--project_view_columns) - 1))
    / var(--project_view_columns) * var(--project_name_columns)
    + var(--grid_gutter) * (var(--project_name_columns) - 1)
  ) / var(--project_title_measure));
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--contact_photo_w);
  column-gap: var(--line_gutter);
  min-height: 100%;
  padding: var(--hero_gap);
  color: var(--charcoal);
}

.contact_figure {
  grid-column: 2;
  grid-row: 1;
  align-self: start;
  margin: 0;
  opacity: 0;
  transition: opacity var(--about_fade_duration) var(--ease) var(--about_fade_delay);
}

.sheet_contact.is_shown .contact_figure { opacity: 1; }

.contact_photo img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--contact_photo_ratio);
}

.contact_list {
  grid-column: 1;
  grid-row: 1;
  align-self: end;
  min-width: 0;
  margin: 0;
}

.contact_item + .contact_item { margin-top: var(--contact_item_space); }

.contact_label {
  margin: 0 0 var(--contact_label_space);
  font-size: calc(var(--project_detail_size) * var(--section_label_scale));   /* the larger label, as DESIGN CHALLENGE: */
  font-weight: var(--weight_bold);
  line-height: var(--project_detail_leading);
  text-transform: uppercase;
}

.contact_line {
  margin: 0;
  font-size: var(--contact_fit, var(--contact_name_size));
  white-space: nowrap;   /* the email is never broken */
}

.sheet .contact_line a {
  position: relative;
  display: inline-block;
  font-weight: inherit;
  text-decoration: none;
}

/* each line's mask hugs its words, so the lines can be measured and sized */
.contact_line .line_mask { display: inline-block; vertical-align: top; }
.contact_line .line_inner { display: inline-block; }

/* hover: a thin underline draws in from the left */
.contact_line a::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(-1 * var(--contact_underline_gap));
  height: var(--contact_underline);
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--contact_underline_duration) var(--ease);
}

.contact_line a:hover::after,
.contact_line a:focus-visible::after { transform: scaleX(1); }

.contact_line a:focus-visible {
  outline: var(--focus_width) solid currentColor;
  outline-offset: var(--focus_width);
}

/* Laptops: the photo runs from the top border margin to the bottom one, its width from its
   shape, leaving the lines at least --contact_lines_share of the page. */
@media (min-width: 1025px) {
  .contact { --contact_photo_w: min((100cqh - var(--hero_gap) * 2) * var(--contact_photo_ratio), 100cqi * (1 - var(--contact_lines_share))); }
}

@media (max-width: 599px) {
  /* the photo takes whatever height the three contacts leave, so all of them fit on one
     screen, cropped around Harvey */
  .contact {
    height: 100%;
    min-height: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
    row-gap: var(--about_section_space);
  }
  .contact_figure { grid-column: 1; grid-row: 1; min-height: 0; height: 100%; }
  .contact_photo { display: block; height: 100%; }
  .contact_photo img {
    height: 100%;
    aspect-ratio: auto;
    object-fit: cover;
    object-position: var(--contact_photo_focus_phone);
  }
  .contact_list { grid-column: 1; grid-row: 2; align-self: start; }
}

@media (prefers-reduced-motion: reduce) {
  .about_body p { opacity: 1; transform: none; transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .contact_figure,
  .contact_line a::after { transition: none; }
}

/* ---------- Mobile project pages (below 768px) ----------
   One column inside a 16px gutter: the close button, then the cover full width at 4:5,
   then the name, very large, sized from the screen width so its widest word just fits,
   then the description, then the year and category as a stacked list, then any further
   images and sections at the same width. The page scrolls. */

@media (max-width: 767px) {
  .project_view_page {
    --grid_margin: var(--project_margin_mobile);
    --name_width: calc(100cqi - var(--project_margin_mobile) * 2);
    --name_size: calc(var(--name_width) / var(--project_title_measure_mobile));
    display: flex;
    flex-direction: column;
    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
  }

  .project_view_close {
    top: var(--project_margin_mobile);
    right: var(--project_margin_mobile);
  }

  .project_view_image {
    flex: none;
    width: 100%;
  }

  .project_view_name { margin: var(--project_title_space_mobile) 0 0; }

  .project_view_details {
    display: flex;
    flex-direction: column;
    gap: var(--project_details_stack_mobile);
    font-size: var(--project_detail_size_mobile);
    line-height: var(--line_body);
  }

  .project_view_first { display: contents; }

  .project_view_description {
    display: block;
    margin: var(--project_details_space_mobile) 0 0;
  }

  .project_view_details { margin-top: var(--project_details_stack_mobile); }

  .project_view_gallery {
    display: flex;
    flex-direction: column;
    gap: var(--project_gallery_gap);
    margin-top: var(--project_gallery_space);
  }

  .project_view_gallery:empty { display: none; }

  .project_section_label { font-size: calc(var(--project_detail_size_mobile) * var(--section_label_scale)); }
  .project_section_caption { font-size: var(--project_detail_size_mobile); }
  .project_section_billboard { width: 100%; }   /* a smaller billboard spans the full width on a phone */

  /* A series becomes one row that scrolls sideways by swiping. */
  .project_series_row {
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
  }
  .project_series_row::-webkit-scrollbar { display: none; }
  .project_series_row > .project_section_image,
  .project_series_row > .project_series_item {
    flex: 0 0 calc(100cqi * var(--series_phone_width));
    scroll-snap-align: start;
  }

  /* A series in rows (If the city could speak…) stacks in pairs instead, two to a row,
     sized to the screen, with no sideways scroll. */
  .project_series_row.is_rows {
    display: grid;
    grid-template-columns: repeat(var(--series_rows_count_phone), minmax(0, 1fr));
    gap: var(--series_rows_gap);
    overflow-x: visible;
    scroll-snap-type: none;
  }
  .project_series_row.is_rows > .project_section_image,
  .project_series_row.is_rows > .project_series_item { flex: none; scroll-snap-align: none; }
  .project_series_row.is_rows .project_series_break { display: none; }

  /* The mockup pair stacks: the landscape image full width with its caption, then the
     portrait one at --pair_right_phone of the width, against the right margin. */
  .project_section_pair { flex-direction: column; align-items: flex-end; gap: var(--project_section_stack); }
  .project_pair_left { width: 100%; }
  .project_pair_right,
  .project_section_pair.is_portraits .project_pair_right { width: calc(100% * var(--pair_right_phone)); margin-top: 0; }

  /* A pinned page on a phone: no pinning. The cover, then the name and labels, the
     description, then the further sections. Both large texts share the smaller phone size. */
  .is_pinned {
    --pinned_lead_scale: var(--pinned_lead_scale_phone);
    --pinned_lead_min: var(--pinned_lead_min_phone);
  }
  .is_pinned .project_view_image { order: 1; }
  .is_pinned .project_view_label { order: 2; display: block; margin-top: var(--project_title_space_mobile); }
  .is_pinned .project_view_name { margin: 0; }
  .is_pinned .project_view_description { order: 3; }
  .is_pinned .project_view_gallery { order: 4; }
  .is_pinned .project_view_sections { order: 5; }
}

/* ---------- Reduced motion ----------
   Banners stop looping and jump to place. The page fades in instead of sliding. */

/* Reduced motion: the logo reveal shows finished, with no movement. */
@media (prefers-reduced-motion: reduce) {
  .project_logo_mark,
  .project_logo_letter { transition: none; }
}

@media (prefers-reduced-motion: reduce) {
  .banner_track { animation: none; }
  .about_figure { transition: none; }
  .reveal { opacity: 1; transform: none; transition: none; }
  .rail_item { transition: none; }
  .sheet.is_shown { animation: sheet_fade var(--fade_duration) var(--ease); }
}

@keyframes sheet_fade { from { opacity: 0; } }

/* ---------- Sheet content ---------- */

.sheet ul,
.sheet dl { margin: 0; padding: 0; list-style: none; }

.sheet_title {
  margin: 0 0 var(--space_5);
  font-size: var(--text_title);
  font-weight: var(--weight_medium);
  line-height: var(--line_tight);
  outline: none;
}

.sheet_subtitle {
  margin: var(--space_6) 0 var(--space_4);
  font-size: var(--text_name);
  font-weight: var(--weight_medium);
}

.text_link,
.sheet a {
  color: inherit;
  font-weight: var(--weight_medium);
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* Placeholders: dashed outlines and the words "To come" mark everything waiting for content. */
.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  border: var(--line_width) dashed var(--charcoal);
}

.placeholder_note { font-size: var(--text_small); }

.placeholder_text { font-style: italic; }
/* Load in animation and the swipe to the hero.
   The strip is two screens wide: the loader on the left, the brown panel on the right.
   It slides left by its full width, so the panel's front edge covers the loader and
   the hero shows behind its back edge. The stage slides the opposite way at the same
   rate, so the monogram holds still while it is covered. */
.loader {
  position: fixed;
  inset: 0;
  z-index: var(--layer_loader);
  overflow: hidden;
}

[data-loader='skip'] .loader { display: none; }

.loader_strip {
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  display: flex;
}

.loader_window {
  position: relative;
  width: 50%;
  overflow: hidden;
}

.loader_stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--pale_yellow);
}

.loader_mark {
  height: var(--loader_mark_height);
  aspect-ratio: 702 / 850;
  width: auto;
  max-width: none;
}

.loader_panel {
  width: 50%;
  background: var(--warm_brown);
}

.loader.is_leaving .loader_strip {
  animation: loader_strip var(--swipe_duration) var(--ease) forwards;
}

.loader.is_leaving .loader_stage {
  animation: loader_stage var(--swipe_duration) var(--ease) forwards;
}

@keyframes loader_strip { to { transform: translateX(-100%); } }
@keyframes loader_stage { to { transform: translateX(200%); } }

/* Phone and tablet: the same swipe turned on its side. The panel sits below the loader and rises
   from the bottom of the screen to the top, revealing the hero beneath it. */
@media (max-width: 1024px) {
  .loader_strip {
    width: 100%;
    height: 200%;
    flex-direction: column;
  }

  .loader_window,
  .loader_panel {
    width: auto;
    height: 50%;
  }

  .loader.is_leaving .loader_strip { animation-name: loader_strip_up; }
  .loader.is_leaving .loader_stage { animation-name: loader_stage_up; }
}

@keyframes loader_strip_up { to { transform: translateY(-100%); } }
@keyframes loader_stage_up { to { transform: translateY(200%); } }
