/* NanoFlashing Air Filter page.

   Built on the Discover NanoFlashing design. discover.css loads first and supplies the
   tokens, the type and the .dn-section / .dn-body contract. This file adds the film behind
   the opening screen, the left rail, and the commercial proof slide.

   Yin, 2026-08-31, in order:
     "use our science page design"
     "The first page background, using https://cpolar.tech/applications/ Air Background"
     "I don't need to cover page for it, just go direct to the content"
     "We shall be like pollutant page, align to left, now left side got a big gap"
     "Make them into one slide, pls carefully design layout"
     "Why no partner logo?"

   Two screens. The opening, and one commercial proof slide holding the paragraph, both
   products and the partner logos. */

/* A brand name must never break across a line. Measured on the phone render: the
   Commercial Proof paragraph split "C-" from "POLAR" at the hyphen. The plain hyphen stays
   in the markup so the text still reads and searches normally; the span stops the break. */
.nb { white-space: nowrap; }

/* ------------------------------------------------------- scrolling, and a real trap */

/* discover.css sets `scroll-snap-type: y mandatory` on <html> and
   `scroll-snap-stop: always` on every section. That works on the science page, where every
   section is exactly one screen. Here the proof section is 1275px in a 900px window, and a
   mandatory snap stop taller than the viewport TRAPS the scroll.
   Measured on the live page: twelve wheel notches of 120px moved it 16px in total and then
   zero. The bottom of the section was unreachable, which is exactly what Yin reported.
   Snapping comes off this page. Two sections, one of them taller than the screen, do not
   need it, and normal scrolling is what he asked for. */
html:has(.af-page) { scroll-snap-type: none; }
.af-page .dn-section { scroll-snap-align: none; scroll-snap-stop: normal; }

/* Except About, which snaps like the science page does. Yin: "it shall be one scroll one
   frame". That is safe here and it was not safe on the applications pages: measured, the
   Commercial Proof section is 1188px of content against a 900px screen, and a section
   taller than the viewport under mandatory snap traps the scroll. Every About section
   fits, measured at 1440x900, 1440x760 and 393x852, with 166px to 600px of content.

   Same specificity as the rule above, (0,1,1), so this has to come after it to win. */
html:has(.af-page--photo) { scroll-snap-type: y mandatory; }
.af-page--photo .dn-section { scroll-snap-align: start; scroll-snap-stop: always; }

/* Yin: "I want to standardize the header position, and body text position".

   The heading rail. It was 409px on the opening and 158px on the other four, because the
   opening was optically centred. Every screen now hangs from the same rail, including the
   first, so nothing moves as you scroll.

   The body rail is the same idea one level down. The headings run one, three, three, two
   and two lines, so the first line of copy landed at five different heights. The heading
   block reserves three lines everywhere, which is the tallest any of them runs, and the
   body starts at one height. */
/* The sections are flex-direction: column, so the vertical axis is justify-content.
   align-items only moved things sideways: measured, the heading tops went 208, 226, 143,
   232 to 252, 271, 306, 277, still four different numbers. */
.af-page--photo.af-page--photo .dn-section,
.af-page--photo.af-page--photo.af-page--photo .dn-section:first-child {
  align-items: stretch;
  justify-content: flex-start;
  padding-top: 130px;
}

/* ---------------------------------------------------------------- the left rail */

/* Measured on the live site at 1440: the pollutant decks start their text 58px from the
   left edge, /applications/ starts at 66px, and this page was starting at 108px because
   discover.css centres .dn-inner inside a --stage that is narrower than the window. That
   108px is the gap Yin saw. The rail below lands on 58px at 1440, matching the decks. */
:root { --af-rail: clamp(20px, 4vw, 58px); }

.af-page .dn-inner {
  width: calc(100% - (var(--af-rail) * 2));
  max-width: 1500px;
  margin-left: var(--af-rail);
  margin-right: var(--af-rail);
}

/* Every block on this page ranges left. Nothing is centred. */
.af-page .dn-inner,
.af-page .dn-body,
.af-page h1,
.af-page h2 { text-align: left; }

.af-measure { max-width: none; }

/* The body measure, taken from Science, which caps its paragraphs at 880px. This has to
   sit on the paragraph, not on .dn-inner: `.af-page .dn-inner` is (0,2,0) and .af-measure
   was (0,1,0), so the old rule never applied and the About paragraphs ran the full
   1325px, about 150 characters a line. Headings stay free, as they are on Science. */
.af-page .dn-body { max-width: min(100%, 880px); text-wrap: pretty; }

/* Codex, defect 5: the link needs "a 44px tap area". The box stays where it is so the
   underline keeps sitting on the word; only the touch target grows. */
.af-link::before {
  content: "";
  position: absolute;
  left: 0;
  right: -8px;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

/* Paragraph rhythm. The old rule was scoped to .af-open, so two paragraphs under a
   section heading ran together with no gap between them at all. */
.af-page .af-section .dn-body + .dn-body { margin-top: 15px; }

/* discover.css sets --gap-head: 24px, tuned for its own centred layout. Anton has almost
   no descender room at line-height 1.06, so about 8px of that gap is swallowed by the
   heading's own line box and the visual gap reads closer to 16px. Ranged left at this
   size it looked cramped, so this page sets its own. */
/* Yin: "Header NanoFlashing XXX shall have a little space between main body text, same
   vertical gap as Commercial Proof with main body text, now too closed".
   Measured: the h2 had 22px on desktop and 20px on the phone, and the h1 had ZERO. The
   reset in discover.css zeroes every margin and my rule only ever named h2, so the product
   name sat straight on top of its first line. Both headings now take the same gap. */
.af-page .af-section h2,
.af-page .af-open__copy h1,
.af-page .af-open__copy h2 { margin-bottom: 22px; }

/* ---------------------------------------------------------------- opening screen */

.af-open {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: #eef5f9;             /* the film's own base, so the poster never flashes
                                      against white while it decodes */
}

.af-open__media { position: absolute; inset: 0; z-index: 0; overflow: hidden; }

/* Both selectors are doubled deliberately.
   discover.css carries `.dn-open img { width: 260px; height: auto }`, written for the
   NanoFlashing logo on its own opening screen. That rule is (0,1,1) and it silently
   captured this background plate: measured live, the poster rendered 267px wide instead of
   filling the screen, which is 260 times the 1.025 scale below. This section never carries
   .dn-open, and these selectors stay specific enough that adding it by accident cannot
   bring the bug back. */
.af-open .af-open__film,
.af-open .af-open__poster {
  display: block;
  width: 100%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  object-position: 58% 50%;        /* matches .v23-chapter__film on /applications/ */
  transform: scale(1.025);         /* hides the edge softening on the source file */
}

.af-open .af-open__poster { position: absolute; inset: 0; margin: 0; }

.af-open .af-open__film {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  transition: opacity .45s ease;
}
.af-open .af-open__film.is-ready { opacity: 1; }

/* The wash, copied value for value from .v23-chapter__wash on /applications/, because that
   gradient was built for this film with body copy over it. Paper on the left where the
   words are, clear on the right where the pleated filter is worth seeing. */
.af-open__wash {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(90deg,
    rgba(250, 253, 255, .97) 0%,
    rgba(250, 253, 255, .84) 29%,
    rgba(250, 253, 255, .34) 57%,
    rgba(250, 253, 255, .06) 100%);
}

/* Codex, defect 4: "The wash erases the photographs. Most are already predominantly
   white... the images become faint atmosphere rather than content." These five are far
   whiter than the pleated-filter film the gradient above was built for, so on this page
   it clears earlier and completely, and the picture past the copy measure is untouched.
   Contrast under the letters is re-measured after this, never assumed. */
.af-page--photo .af-open__wash {
  background: linear-gradient(90deg,
    rgba(250, 253, 255, .96) 0%,
    rgba(250, 253, 255, .90) 26%,
    rgba(250, 253, 255, .40) 48%,
    rgba(250, 253, 255, 0) 64%);
}

.af-open .dn-inner { position: relative; z-index: 2; }
.af-open__copy { width: min(650px, 48vw); }

/* ---------------------------------------------------------------- the hammer line */

/* "Nothing warns you." carries the turn in the argument. It is the one line on the page
   allowed to stand alone. Weight, not size, so it never competes with a heading. */
.af-hammer {
  margin: 24px 0 0;
  font-family: var(--type);
  font-size: 19.5px;
  font-weight: 700;
  line-height: 1.62;
  color: var(--ink);
}
.af-open .af-hammer + .dn-body { margin-top: 20px; }
.af-open .dn-body + .dn-body { margin-top: 15px; }

/* ---------------------------------------------------------------- link rows */

.af-link {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: .5em;
  margin-top: 22px;
  font-family: var(--type);
  font-size: 13.5px;
  font-weight: 650;
  letter-spacing: .01em;
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
}
.af-link:hover,
.af-link:focus-visible { color: var(--ink); }

/* The section inherits a large top padding from .dn-section, sized for the science page's
   centred layout. Ranged left with a slide this full it wasted room the logo strip needed
   at the bottom. */
.af-page .dn-section { padding-top: 62px; padding-bottom: 52px; }
.af-page .dn-section:first-child { padding-top: calc(var(--header) + 52px); }

/* ---------------------------------------------------------------- the proof slide */

/* One screen carrying three things Yin asked to merge: the Commercial Proof paragraph,
   both products, and the partner logos. The height budget on a 900px window is about
   690px once the section padding and the fixed header are taken out, so the products lie
   on their side, picture beside words, rather than stacking picture over words. That is
   what makes all three fit without shrinking the type. */

.af-proof__lede { max-width: 58ch; margin-bottom: 28px; }

.af-rule {
  height: 1px;
  margin: 0 0 28px;
  background: var(--rule);
}

.af-products {
  display: grid;
  gap: clamp(26px, 3.4vh, 40px);
}

/* Yin, 2026-08-31: "We can design that page like validation site, so picture can be
   large, right?" Yes. This is the Validation page's own split, words on the left and a
   large picture running out to the right, applied per product. The picture goes from
   280px wide to roughly 660px, which is what Codex asked for and what the products need
   to read as real manufactured goods rather than thumbnails. */
.af-product {
  display: grid;
  grid-template-columns: minmax(0, 0.86fr) minmax(0, 1fr);
  gap: clamp(26px, 3vw, 52px);
  align-items: center;
}

.af-product__media {
  position: relative;
  aspect-ratio: 16 / 9;      /* the film's own ratio, so contain neither crops nor pads */
  overflow: hidden;
}
.af-product__media video,
.af-product__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The crops are measured, not eyeballed.
   ReinFire plate, 1280x720: the orange filter sits at x 11-60%, y 13-87% of the source,
   and the product was only 22.7% of the pixels with the right 40% empty grey.
   First attempt used 30% / scale 1.18. Measured in the RENDERED frame that put the filter
   at 4-61% across and 1-99% down: not clipped, but pressed against the top and bottom and
   leaving 39% dead grey to its right. 42% / 1.05 centres it and gives it air.
   INHALO photo: the unit sits at x 64-89%, y 22-68% of the source, only 24% of its width.
   First attempt used 77% / scale 1.5, which measured at 72-100% across in the rendered
   frame, running hard into the right edge. 62% / 1.35 brings it inside. */
.af-product--reinfire .af-product__media video,
.af-product--reinfire .af-product__media img {
  object-fit: contain;          /* Yin: "don't crop the product video". The frame is now
                                   the film's own 16:9, so nothing is cut and nothing is
                                   letterboxed. No zoom, no reposition. */
}
.af-product--inhalo .af-product__media img {
  /* object-position X does nothing here: the source is 1.79:1 inside a 2.2:1 frame, so
     `cover` fills the width and crops only vertically. Measured after changing it from 77%
     to 62% the unit had not moved, still at 53-100% across. Moving it takes a translate.
     Subject centre measured at 76% of the frame; -19% before the 1.35 scale lands it near
     the middle. */
  /* No transform. products-inhalo.js computes every particle position from `cover`
     geometry and this element's object-position, and has no knowledge of a CSS transform,
     so any scale or translate here puts the whole airflow animation in the wrong place.
     `right center` is what the original page used and what the script is written against.
     It also means there is nothing to leave a gap at either edge. */
  object-position: right center;
}

.af-product__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 0 0 10px;
}

.af-product__name {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(24px, calc(15px + .95vw), 30px);
  font-weight: 400;
  line-height: 1.06;
  color: var(--ink);
}

.af-product__cat {
  font-family: var(--type);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: .03em;
  color: var(--ink-soft);
}

/* No size override. The product paragraph is a .dn-body and now reads at the same
   19.5/1.62 as every other paragraph on the site. */

/* The air intake animation, drawn on a canvas over the INHALO photograph by
   products-inhalo.js. It only runs while its article carries `is-active`, which the old
   pager set and air.js now sets from an IntersectionObserver. */
.inhalo-airflow {
  position: absolute;
  inset: 0;
  z-index: 2;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
  transition: opacity .35s ease;
}
.af-product--inhalo.is-active .inhalo-airflow { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .inhalo-airflow { display: none; }
}

/* ---------------------------------------------------------------- a full-width film */

/* The strawberry test on the food packaging page. The film is a three-column comparison
   with small labels over each column, so it has to run wide or the labels stop being
   readable. No border and no panel colour: the frame matches the source exactly, so there
   is nothing to letterbox. */
.af-film {
  margin: clamp(22px, 3vh, 34px) 0 0;
  padding: 0;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: #b0b0b0;
}
.af-film video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .af-film video { display: none; }
  .af-film { background: #b0b0b0 url("/food-packaging/assets/strawberry-poster.jpg") center/cover; }
}


/* The body rail. Three lines is what the tallest heading runs at 1440, so reserving
   three everywhere puts the first line of copy at the same height on every screen. The
   unit is em, so it follows the heading down the breakpoints on its own. */
@media (min-width: 901px) {
  /* Yin: "For the first page, pull the header lower, the gap between header and body text
     is too big." The block still reserves three lines so the body starts at one height on
     every screen, but the heading now sits at the BOTTOM of it. A one-line heading drops
     down to meet its own copy instead of leaving two empty lines under itself. */
  .af-page--photo .af-open__copy h1,
  .af-page--photo .af-open__copy h2 {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-height: calc(4 * 1.06em);
  }
}

/* ------------------------------------------------------- the split, screen four */

/* Codex, second pass: "Five near-identical compositions -- not fixed. Screens 2-5 still
   share the same macro-template: heading at 158px, copy on the 58px rail, white wash,
   photograph emerging on the right." Its fix: "Make screen 4 a hard two-column
   interruption. Keep the heading/grid on white and place the existing product photograph
   in a sharply bounded right panel with no gradient. One section modifier is enough."

   This is that modifier. The picture stops bleeding across the screen and takes a panel
   with a hard left edge, the wash goes entirely, and the words sit on plain paper, which
   is also what connects this screen to the white sections on Science and Applications.

   Desktop only. At 393px there is no side by side, so the phone keeps the paper over
   picture treatment every other section has. */
/* Yin: "the image, left side shall have fading effect, so we can have soft transition".

   A white wash painted over a photograph is not a fade, it is a veil, and it left a
   visible transition band where the words ended. The photograph itself is masked instead,
   so its left side dissolves into the paper and there is nothing laid on top of it. The
   paper behind is the real page colour, not a 96% approximation of it.

   The mask is fully clear to 22%, so the words sit on plain paper, and fully opaque from
   68%, so the subject of every picture is untouched. */
@media (min-width: 901px) {
  .af-page--photo .af-open { background: var(--paper); }

  .af-page--photo .af-open__wash { display: none; }

  /* Clear to 48%. The copy measure ends at 708px of 1440, which is 49%, and the partner
     logo row ends at about the same place. Starting the fade at 22% put the technician,
     his sleeve and the rollers underneath the words and behind the logos. Nothing of the
     photograph now begins until every line of copy has ended. */
  /* Yin: "fading transition is not large enough, now transition is too huge." It ran
     from 48% to 80%, finishing in a third of the width, and you could see where it
     landed. It now runs from 50% all the way to 96%: nothing begins until the copy has
     ended at 49%, and there is no point along it where the picture arrives. */
  /* Codex: "only the final 4% reaches full strength. The result reads as perpetual fog,
     not restraint." The fade was 50% to 96%, so the photograph never actually arrived.
     Clear to 50%, which is still past the 49% copy measure, full by 76%, then held. A
     26% fade, still soft, and a quarter of the width at full strength. */
  .af-page--photo .af-open__media {
    -webkit-mask-image: linear-gradient(90deg,
      rgba(0, 0, 0, 0) 50%,
      rgba(0, 0, 0, .22) 58%,
      rgba(0, 0, 0, .58) 66%,
      rgba(0, 0, 0, .88) 71%,
      rgb(0, 0, 0) 76%);
            mask-image: linear-gradient(90deg,
      rgba(0, 0, 0, 0) 50%,
      rgba(0, 0, 0, .22) 58%,
      rgba(0, 0, 0, .58) 66%,
      rgba(0, 0, 0, .88) 71%,
      rgb(0, 0, 0) 76%);
  }

  /* Screen four holds its picture instead of cropping it. Four objects in a row cannot
     survive a cover crop into half a screen: the fibre tuft was cut off entirely and the
     roll was clipped at the screen edge. Contained, in a panel on the right, the whole
     line-up shows at its own proportions. The photograph's own background is near-white,
     so its top and bottom edges melt into the paper with nothing added. */
  /* Contained, this photograph letterboxes, and the top and bottom of its own near-white
     background showed as a faint grey band against the page. Two masks, intersected, so
     the panel softens on every side and there is no rectangle. */
  /* Codex: "Desktop has a conspicuous floating image rectangle... contain it inside the
     same full-height right-side media stage used everywhere else. Extend its neutral
     background vertically so the rectangular top and bottom edges disappear."

     The panel starts at the same 50% as every other screen, and the vertical fade is 22%
     at each end instead of 9%, which is what it takes to lose a hard edge on a plate this
     close to white. */
  .af-page--photo .af-open--whole .af-open__media {
    inset: 0 0 0 50%;
    -webkit-mask-image:
      linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.22) 8%, rgba(0,0,0,.72) 20%, rgb(0,0,0) 32%),
      linear-gradient(180deg, rgba(0,0,0,0) 0%, rgb(0,0,0) 22%, rgb(0,0,0) 78%, rgba(0,0,0,0) 100%);
    -webkit-mask-composite: source-in;
            mask-image:
      linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,.22) 8%, rgba(0,0,0,.72) 20%, rgb(0,0,0) 32%),
      linear-gradient(180deg, rgba(0,0,0,0) 0%, rgb(0,0,0) 22%, rgb(0,0,0) 78%, rgba(0,0,0,0) 100%);
            mask-composite: intersect;
  }
  .af-page--photo .af-open--whole .af-open__poster {
    object-fit: contain;
    object-position: 50% 50%;
    transform: none;
  }
}

/* ------------------------------------------------- partner logos on the About page */

/* The same five logos, the same verified links and the same optically balanced heights
   as the air page, but inside a 650px copy column rather than a 1325px band, so the row
   is tighter and the label sits closer. */
.af-page--photo .af-partners {
  margin-top: 18px;
  max-width: 650px;
}
.af-page--photo .af-partners__label { margin-bottom: 10px; }
/* Yin: "logo is not equally distributed, you can use top 3 bottom 2, so each logo can be
   larger." Three across, then two. The heights below keep the measured optical ratio from
   the air page, all raised by 35%, so the row still reads even rather than five logos at
   the same nominal height and five different weights. */
/* Columns sized to their content, not three equal thirds. Left-aligned in equal columns,
   a narrow logo left a wide hole after it and the row read unevenly, which is what Yin
   called "not equally distributed". One fixed gap between them spaces them evenly and the
   row still starts on the page rail. */
/* Six columns, three items spanning two each on the first row and two spanning three
   each on the second. That is the same construction the air page uses on a phone, and it
   gives three across then two with every logo centred in an equal share and both rows the
   same width. max-content columns sized to the widest item in each column across BOTH
   rows, which is why the measured gaps came out 46px and 108px. */
.af-page--photo .af-logos {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  justify-items: center;
  align-items: center;
  gap: 8px 0;
  border: 0;
  max-width: 620px;
}
.af-page--photo .af-logos li { grid-column: span 2; }
.af-page--photo .af-logos li:nth-child(n+4) { grid-column: span 3; }
.af-page--photo .af-logos a {
  min-height: 62px;
  padding: 2px 0;
  border: 0;
  justify-content: flex-start;
}
/* 1.2x the air page heights, down from 1.35x, because at 1.35x this screen's content
   ended at 910px in a 900px viewport. Still larger than they were, and the ratio between
   them is the measured optical one, so the row reads even. */
.af-page--photo .af-logos li:nth-child(1) img { max-height: 37px; }  /* Aero Pro */
.af-page--photo .af-logos li:nth-child(2) img { max-height: 50px; }  /* AQ Filter */
.af-page--photo .af-logos li:nth-child(3) img { max-height: 70px; }  /* BGE */
.af-page--photo .af-logos li:nth-child(4) img { max-height: 62px; }  /* Olympic */
.af-page--photo .af-logos li:nth-child(5) img { max-height: 37px; }  /* VOLZ */

@media (max-width: 900px) {
  .af-page--photo .af-logos { max-width: none; gap: 6px 0; }
  .af-page--photo .af-logos a { min-height: 58px; }
  .af-page--photo .af-logos li:nth-child(1) img { max-height: 30px; }
  .af-page--photo .af-logos li:nth-child(2) img { max-height: 40px; }
  .af-page--photo .af-logos li:nth-child(3) img { max-height: 54px; }
  .af-page--photo .af-logos li:nth-child(4) img { max-height: 49px; }
  .af-page--photo .af-logos li:nth-child(5) img { max-height: 30px; }
}



/* ---------------------------------------------------------------- the phone card */

/* On a phone there is no left half to range the words into: the picture fills the width
   and so does the copy. The wash strong enough to carry body text is .90, and measured
   at that strength the pictures are gone. A sweep confirmed there is no middle: at .78
   the worst contrast under a letter falls to 2.58:1, at .70 to 2.11:1, against a 4.5 bar.

   So the pollutant decks' answer, which is what Yin pointed at: the words sit on a paper
   card over the picture, and the picture shows around it. The wash drops to a light tint
   that only steadies the edges.

   Scoped to .af-page--photo, which only the About page carries. The five application
   pages keep the wash they were signed off with. */
@media (max-width: 900px) {
  .af-page--photo .af-open__wash {
    background: linear-gradient(180deg,
      rgba(250, 253, 255, .10) 0%,
      rgba(250, 253, 255, .22) 55%,
      rgba(250, 253, 255, .40) 100%);
  }

  /* Codex, defect 2: "The phone card is a patch... The rounded floating rectangle
     introduces a generic SaaS component into a sharp, restrained system. It also creates
     three competing rails: screen edge, card edge, and text edge."

     So the paper is no longer a card. It squares off, runs the full width of the screen,
     and belongs to the section. That leaves two rails, the screen edge and the text, and
     the text keeps the same rail it has on desktop. */
  .af-page--photo .af-open__copy {
    /* width:100% from the rule above is a width on .dn-inner's 353px box, so the negative
       margins moved the paper left instead of widening it and it stopped 60px short of
       the right edge. auto lets the margins do their job. */
    width: auto;
    /* The transition is vertical here, not horizontal, so the paper's bottom edge is what
       fades into the photograph. The fade lives entirely inside the bottom padding, so no
       line of text ever sits on it. */
    background: linear-gradient(180deg,
      rgba(252, 254, 255, .95) 0%,
      rgba(252, 254, 255, .95) calc(100% - 46px),
      rgba(252, 254, 255, 0) 100%);
    border-radius: 0;
    margin-left: calc(-1 * var(--af-rail));
    margin-right: calc(-1 * var(--af-rail));
    padding: 26px var(--af-rail) 52px;
  }

  /* "Consistent, intentional image reveal below or beside the paper" and "titles
     top-aligned at least 24-32px below the fixed header." The paper sits at the top of
     every section and the same strip of photograph shows beneath it, every time. */
  /* Codex: "The mobile sections end with 40-45px horizontal photograph remnants... make
     it a deliberate, consistent 90px band." Measured, they were 44px on every section,
     because this rule was losing the same specificity tie. 96px, every section. */
  /* Every section gets header clearance, not just the first. With snapping on you land
     on all five, and the bar is fixed: measured, the heading top on sections 2 to 5 sat
     at 53px against a 67px bar, so the first line was behind it. */
  /* Codex: "Each snapped section should occupy at least 100svh minus the fixed header...
     Currently screen 1 exposes the next heading." Measured, screen 1 was 514px tall in an
     852px viewport, so the next screen's picture and heading were already on screen when
     you landed on the first. svh, not vh, so the phone's collapsing address bar does not
     change the stop height mid-scroll. */
  .af-page--photo.af-page--photo .dn-section,
  .af-page--photo.af-page--photo.af-page--photo .dn-section:first-child {
    display: block;
    min-height: 100vh;
    min-height: 100svh;
    padding-top: calc(var(--header) + 28px);
    padding-bottom: 96px;
  }

  /* Screen 5 cannot fit a phone screen: 904px of content in an 852px viewport, and the
     copy alone is 792 of it. A heading, two paragraphs and five logos do not go into
     852px. Forcing a stop there means one flick jumps to the footer and the logo row is
     never seen, so this one section lets you scroll through it. */
  .af-page--photo .dn-section:nth-of-type(5) {
    scroll-snap-align: start;
    scroll-snap-stop: normal;
  }

  /* And the same reserved heading block as the desktop, so the body starts at one height
     here too. 120px is the four lines the tallest phone heading runs. */
  .af-page--photo .af-open__copy h1,
  .af-page--photo .af-open__copy h2 {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-height: 120px;
  }

  .af-page--photo .af-open__wash {
    background: linear-gradient(180deg,
      rgba(250, 253, 255, .06) 0%,
      rgba(250, 253, 255, .14) 60%,
      rgba(250, 253, 255, .30) 100%);
  }

  .af-steps dt, .af-steps dd { grid-column: 1; }
}

/* ---------------------------------------------------------------- citation links */

/* The citations inside the argument had no rule at all, so they fell back to the
   browser default: link blue, visited purple. Yin wants them black. The underline
   stays, because with the colour gone it is the only thing left telling a reader
   this is a link, and it is the only cue a colour-blind reader ever had. */
.af-page .dn-body a,
.af-page .dn-body a:visited {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: rgba(31, 35, 39, .38);
  transition: text-decoration-color .18s ease;
}
.af-page .dn-body a:hover,
.af-page .dn-body a:focus-visible {
  color: var(--ink);
  text-decoration-color: var(--ink);
}

/* ---------------------------------------------------------------- the steps list */

/* Four short steps on the About page. A description list, because that is what it is: a
   label and the line that belongs to it. No numbers, because Yin asked for none. */
/* Codex, defect 5: "Screen 4's rules resemble an accordion or specification table
   despite not being interactive... replace with an actual label/statement grid." So the
   rules go entirely. A label column and a statement column carry the structure, which is
   also what the rules were only pretending to do. */
/* Yin: "And 4 value, could you have nicer way to put them together, they are company core
   value." Four things of equal standing should read as a set, not as a list of four. Two
   by two, label over statement, nothing between them but air. */
.af-page--photo .af-steps {
  margin-top: 0;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(22px, 3vh, 34px) 40px;
  align-items: start;
  max-width: 640px;
}
.af-page--photo .af-steps > div { display: block; }
.af-page--photo .af-steps dt,
.af-page--photo .af-steps dd { grid-column: auto; }
.af-page--photo .af-steps dt { margin-bottom: 7px; }

.af-steps {
  display: grid;
  grid-template-columns: 128px minmax(0, 1fr);
  gap: 16px 24px;
  align-items: baseline;
  margin: clamp(22px, 2.8vh, 32px) 0 0;
  padding: 0;
  border: 0;
}
.af-steps dt {
  margin: 0;
  grid-column: 1;
  padding: 0;
  font-family: var(--type);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: .04em;
  line-height: 1.5;
  color: var(--ink-soft);
}
.af-steps dd {
  grid-column: 2;
  margin: 0;
  padding: 0;
  border: 0;
  font-family: var(--type);
  font-size: 19.5px;
  font-weight: 450;
  line-height: 1.62;
  color: var(--ink);
}

@media (max-width: 900px) {
  .af-steps dt { font-size: 12px; line-height: 1.3; }
  .af-steps dd { font-size: 15px; line-height: 22px; }

  /* One column. Two columns inside 393px leaves about 156px per value, which measured out
     at roughly 20 characters a line, so every statement ran to five lines of three or
     four words. Two by two is right on a desktop and wrong here.

     This has to sit AFTER .af-page--photo .af-steps above: both are (0,2,0), so the later
     one wins, and the first attempt was placed before it and lost. */
  .af-page--photo .af-steps {
    grid-template-columns: minmax(0, 1fr);
    gap: 22px 0;
    max-width: none;
  }
  .af-page--photo .af-steps dt,
  .af-page--photo .af-steps dd { grid-column: 1; }
}

/* ---------------------------------------------------------------- partner logos */

/* Yin: "Why no partner logo?" The five files already on the site, in the bordered grid the
   old air page used, so the treatment is not new. The label above them names whose
   partners these are, because they are ReinFire's makers and distributors and must not
   read as INHALO's. */

.af-partners { margin-top: clamp(20px, 2.6vh, 30px); }

.af-partners__label {
  margin: 0 0 14px;
  font-family: var(--type);
  font-size: 14.5px;
  font-weight: 600;
  letter-spacing: .05em;
  color: var(--ink-soft);
}

.af-logos {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
}
.af-logos li {
  display: grid;
  min-width: 0;
}

/* Yin: "pls help me to make them clickable to their own website". The link fills the whole
   cell so the target is the cell, not just the artwork, which matters most on a phone.
   The logos are other companies' marks, so the hover is a lift in opacity only. Nothing
   recolours or outlines them. */
.af-logos a {
  display: grid;
  place-items: center;
  min-height: clamp(68px, 8vh, 86px);
  padding: clamp(10px, 1.2vw, 16px);
  opacity: .82;
  transition: opacity .2s ease;
}
.af-logos a:hover,
.af-logos a:focus-visible { opacity: 1; }
.af-logos a:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.af-logos img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  object-fit: contain;
}
.af-logos li:nth-child(1) img { max-height: 31px; }  /* Aero Pro, 4.06:1, coverage .297 */
.af-logos li:nth-child(2) img { max-height: 42px; }  /* AQ Filter, 1.49:1, coverage .511 */
.af-logos li:nth-child(3) img { max-height: 58px; }  /* BGE, 1.00:1, coverage .444 */
.af-logos li:nth-child(4) img { max-height: 52px; }  /* Olympic, 2.19:1, coverage .197 */
.af-logos li:nth-child(5) img { max-height: 31px; }  /* VOLZ, 3.69:1, coverage .354 */

/* ---------------------------------------------------------------- phone and tablet */

@media (max-width: 900px) {
  /* The copy column becomes the whole screen, so a left-to-right wash would leave the last
     words on open film. Turn it down the page and let it reach paper at the bottom, which
     also removes the join into the next section. */
  .af-open__copy { width: 100%; }

  .af-open__film,
  .af-open__poster { object-position: 50% 50%; }

  .af-open__wash {
    background: linear-gradient(180deg,
      rgba(250, 253, 255, .30) 0%,
      rgba(250, 253, 255, .88) 18%,
      rgba(250, 253, 255, .94) 70%,
      rgb(255, 255, 255) 100%);
  }

  .af-hammer { font-size: 15px; line-height: 22px; }

  /* 148px of padding above the first heading is desktop air on a 393px screen. */
  .af-page .dn-section { padding-top: 46px; padding-bottom: 44px; }
  .af-page .dn-section:first-child { padding-top: calc(var(--header) + 40px); }
  .af-page .af-section h2,
  .af-page .af-open__copy h1,
  .af-page .af-open__copy h2 { margin-bottom: 20px; }
  .af-proof__lede { margin-bottom: 24px; }
  /* Fitting one viewport was driving the leading. Better to let the link fall below the
     fold than to crush the argument. */
  .af-open .dn-body + .dn-body { margin-top: 18px; }
  .af-open .af-hammer { margin-top: 26px; }
  .af-open .af-hammer + .dn-body { margin-top: 24px; }

  /* Picture over words on a phone, but the NAME first. Reading a picture before knowing
     what it is weakens the scan order, which is what the gate caught. */
  /* A 112px thumbnail beside the words left a 225px text column, about 28 characters a
     line, which reads as broken prose. Stacking with the picture height capped costs about
     40px and restores a proper measure. */
  .af-product { grid-template-columns: minmax(0, 1fr); gap: 12px; align-items: start; }
  .af-product__media { aspect-ratio: 16 / 9; height: auto; }
  /* no size override: the product paragraph is a .dn-body at 15 / 22px like the rest */
  .af-product__head { margin-bottom: 6px; }

  .af-logos { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .af-logos a { min-height: 64px; padding: 6px 4px; }
  .af-logos li { grid-column: span 2; }
  .af-logos li:nth-child(n+4) { grid-column: span 3; }
  .af-logos li:nth-child(1) img { max-height: 27px; }  /* Aero Pro */
  .af-logos li:nth-child(2) img { max-height: 32px; }  /* AQ Filter */
  .af-logos li:nth-child(3) img { max-height: 44px; }  /* BGE */
  .af-logos li:nth-child(4) img { max-height: 42px; }  /* Olympic */
  .af-logos li:nth-child(5) img { max-height: 25px; }  /* VOLZ */
}

@media (max-width: 760px) {
  .af-hammer { font-size: 15px; line-height: 22px; margin-top: 20px; }
  .af-link { font-size: 12px; margin-top: 18px; }
}

/* ---------------------------------------------------------------- reduced motion */

/* Motion is optional here, never load-bearing. With motion reduced the posters carry the
   screens and no film starts. */
@media (prefers-reduced-motion: reduce) {
  .af-open__film { display: none; }
  .af-product__media video { display: none; }
}
