Landing story
The public page's one long argument, told once, in a stage that holds still while the reader scrolls. A round window of fourteen subscriptions contracts into the list this product gives you, and then three of them are cancelled in front of you while the total steps down and the reason for each one is stated beside it.
What it is
It replaced two components, and the reason is the founder's. Earlier the same day this page carried a field of squares in the hero and a list of the same fourteen further down. They said one thing in two places and moved the reader between them with a cut. A hand-off between two blocks a screen apart is a cut; a stage that stays put while the content inside it changes is a shot, and the difference is the whole effect. Both files were retired in the same step, with their pages, their registry rows and their imports: a component that no page holds does not exist.
The shape is the argument, twice. A field says a subscription stack is not something anybody sat down and made: it arrives, one charge at a time, from more directions than a person is watching. A list says what the product does about it: same fourteen, same prices, one column, one total. The reader watches the second thing be made out of the first, which is a demonstration rather than a claim.
The number moves only when something visible is cancelled. $192.90 to $174.91 to $161.92 to $144.92, and every step is the price on the line that just went quiet. Nothing here counts up on its own. A number that changes for a reason a person can check is a demonstration; a number that changes by itself is a claim, and this product does not make claims at people who already do not trust finance apps.
The fourteen and the three are both grounded. The fourteen, their categories and their prices are the "Canonical subscription dataset" in voice/docs/microcopy.md, which has owned them since the voice stage and sums to $192.90. The three that go are the three the product itself names: cancel-win cancels Netflix and frees $17.99; home flags The New York Times at six weeks unopened and Peloton App at three, and its own line "save up to $29.99 by cutting 2" is those two added up. The script that writes the markup reads the fixture table and refuses to write anything that disagrees with it.
Anatomy
.lp-storythe section. It declares the timeline every part of the stage reads, and it is tall only where the stage can work.storypinthe stage: everything the reader sees during the story is inside this one box, which is what makes it a shot rather than a sequence of blocks.storyheadthe heading, the paragraph, the label, the total, the count and the caption. It does not animate: it is the anchor, and a thing that stays does not make an entrance.storystackfour totals in one cell, and four counts in another. CSS cannot rewrite the words inside an element, so every value it will take is in the markup and the scroll decides which one is opaque.storylistfourteen.sline. Mark, name, price, state. Three of them carry.cut1,.cut2,.cut3and a.swapholding both chips.storyasidesthe three benefit cards, which are the copy this section always carried. Each arrives just before the cancellation it explains.sline.cuta line that is about to go. It does not just go grey: it recedes, its chip turns to "Cancelled", it holds for a beat so the word can be read on a row that is still there, and then it folds upward out of the list and the lines under it come up to fill the space. One keyframe set and not two, because both halves would be an animation on the same node andfill-mode: bothmeans the second simply replaces the first. Collapsing is an animation onheight, so the row has to be given a height to animate from ---srow-h, declared on the list and inherited - becausemin-heightis a floor andautocannot be interpolated
It has a second shape, .fromcircle, and since 2026-08-15 it is the ONLY shape with a page. On the landing the hero has already shown the fourteen in a round window, so the stage opens on that same window and contracts it into the list rather than gathering three tracks. The section is 320svh there, cut from 560 in two steps on the founder's mark because the three cancellations were too far apart under the hand: every range on that stage is a percentage of the contain phase, so shortening the section moves nothing about the choreography and only changes how far the reader has to travel to see it - the pinned part goes from 460svh to 220, and the gap between two cancellations from 64svh to 31. The stage there also carries no centred head: the label, the total, the count and the caption are already in the hero's two bottom corners, so the stage keeps them in the same two corners and counts them down there, and its own head is the heading and its paragraph, quiet and left aligned. The centred hero did not win: the founder chose the round window and it became design/index.html, so the modifier is what renders and the shape it modifies is what no page writes any more. .storyfigs and .storystack .count were deleted the same day, because they were two rules with nothing depending on them; .storyfield, .srow, .strack and .stile reached twelve places in the file, so folding them out was left as a row in docs/backlog.md rather than done as a side effect of moving a page.
That row was closed on 2026-08-16, before stage 11, and it was closed in halves. The dead shape is deleted: .storyfield, .srow, .strack, .stile, @keyframes sdrift, @keyframes sgather and the four rules that switched them off, 89 lines, written by no page in either corpus. The reason it could not wait is the stage that comes next: the first thing stage 11 does is take an inventory of what moves, and an inventory taken over rules that never run measures the wrong product. The class name stays, and that half of the row is answered no rather than left open: dropping .fromcircle would be a rename across 257 selector sites, 59 here and 198 in landing-orbit.css, every one carrying a scroll-driven rule and every one moving from 0-3-0 to 0-2-0 in a file whose cascade was tuned in place. It buys a shorter name and risks the one thing on this page that cannot be checked by looking at a still. Proved inert: the stage was captured at twelve scroll positions on six viewports in both themes, before and after, and every difference that survived a second capture of the same code was text-width jitter from font loading. The section is 2880px at 1440 and 2701 at 390 on both sides of the cut.
On the candidate the list is a deck rather than a table. The founder asked for it in those words - "каскадний блок, знизу вверх, 3д", with a cancelled line flying up and out and a new one arriving from below - and three things make it, none of them a library. A frame of twelve rows and not fourteen, so two of the fourteen start below its edge and the thirteenth rises into view on its own when Netflix folds away: the arrival is the flow doing its work, and no subscription had to be invented for it. Twelve and not ten, because the second and third cancellations are the 11th and 13th lines and both have to be inside the frame at their own moment. A recline, perspective() before rotateX(4deg) with the origin at the foot, appended to the list's own travel through var(--stilt,) - the empty fallback is what makes the variable optional rather than required, and without it the whole transform is invalid and the list does not move at all. And two fades, top and bottom, so a row is never cut by a hard line at either end.
Behaviour on width
Two points. At the registered 47.5rem the line goes from 32 to 40 and the lead from --type-meta to --type-body: below it, 32 and the smaller lead are what let the head, fourteen lines and a card share one phone screen. At 80rem the cards leave the foot of the list and stand beside it: the list is 620 wide, so its edge is 310 from the centre and 24 of air puts a card's inner edge at 334, which stops leaving room for a card somewhere under 1260. The list's travel is not a point at all. Whether fourteen lines fit is a question about height, which no width query can answer, so the distance is read off the stage itself with cqh and is exactly zero when nothing overflows.
Limits
The swap's track is minmax(0, 1fr), and this is the third time that sentence has had to be written in this file. A default auto track sizes to its children's MAX-content, and the max-content of a row of the fourteen is its mark, its whole name, its price and its badge with nothing wrapped: 310px. Handed a 242px box at a 320px window the track came out 310 anyway and put 31px of sideways scroll on the document, in both themes. The row already knows how to be narrow - .sline .name has min-width: 0 and an ellipsis - and it never got the chance, because the track above it never asked it to. The same shape once cost 2664px of sideways scroll in reduced motion, where the fix went into the release; it belongs on the base rule, and it is there now.
It owns .sline, and that is deliberate. The product's .row is 58px tall, so fourteen of them are 812 and no viewport holds the list plus its head. Restyling .row inside a host is the contextual override the architecture forbids, and a story line is a different object with a different job: it is read as a field of fourteen, never tapped, never a target. So it is declared here, out of the same atoms, at the height the stage can hold. Its logo size lives in logo.css with the other host sizes, not here.
Every timed line is behind @supports (animation-timeline: view()), including the height and the pin. Unguarded, the declarations stay valid, every animation falls back to the document timeline, and the whole story plays itself once on load at the top of the page where nobody is looking. The height is inside the guard for the same reason in reverse: five screens of scroll through one frozen frame is worse than no effect at all. Without support the section lays out as field, list, three cards, stacked and readable, at $192.90 with nothing cut.
The range is contain and never cover. contain is exactly the part of the scroll where the section covers the viewport, which is exactly the part where the pin is stuck. Written as a cover percentage the beats land at different moments on a phone and on a desktop, because the section is a different height on each. That mistake was made twice on this page before this component existed.
The four figures never overlap, and that is a correction. The first cut cross-faded each pair over the same six per cent, so for a third of that window both numbers stood at half opacity in one cell and $192.90 read as a smudge behind $174.91 at the largest size on the page. shand holds at zero until the outgoing value has finished leaving, then takes one per cent to arrive. No frame has two totals on it and no frame has none.
Every moment rule is written at the specificity of the rule it times, and after it. The generic chip rules are 0-4-0; a moment written as .cut1 .swap > * is 0-3-0, loses, and all three cancellations fire at once. The same mistake in the reduced-motion block leaves one "Cancelled" chip standing. Both have happened here.
Two mobile defects, and both are one shape written for a wide layout
The figures ran down the list, and it is this component's own sentence read over the SET. Founder, on a phone, 2026-08-17: “баг, бегают цифирки, такого не должно быть, давай к правому краю”. .sline was a flex line, so the state cell was sized by its own label and nothing else - Active measures 45 and the Cancelled stack 62 - and the state is the LAST item, so its right edge is the row's and its left edge moves. The amount sits immediately before it. Measured at 320, the price stood at two different offsets from its own row's right edge, 78 and 61, so fourteen figures read as a column that wobbles. The note under .swap has said since it was written that the cell is sized by the wider of its two labels so that nothing to its left moves when the state changes: true within one row, and never true across the list.
So the row is a grid and the last track is a number. 62px, the measured width of the widest state cell here. Fixing that one track fixes the amount without touching the amount: the price is still sized by its own digits, and because the track after it no longer moves, every right edge lands on one line. tabular-nums was already doing its half and had nothing to align against. minmax(0, 1fr) for the name and not 1fr, for the reason this file records twice already: a track's automatic minimum is its content, so a long name would push the price out instead of ellipsising.
Measured after: one distinct offset at 320, 360, 390, 430 and 760, against two before, and the same eight names ellipsise before and after, so the fixed track cost the name column nothing. The widest state cell reads 62 on a phone and 60 at the desk, and 0 of 14 rows have the state overlapping the amount at any of seven widths. The twelve distinct edges past 1024 are not this defect and were not fixed: the stage scales each plate on a ramp, --pscale per nth-child, so the rows are deliberately different widths there.
And the pair of figures had one block in two alignments. Founder, same hour: “тут тоже на мобилке не понятно что и зачем, как баг выглядит”. The count and the total sit in two columns of 143px on a phone, which is deliberate, but the total kept the text-align: right it wears in the WIDE layout, where it is pinned to the far corner of the stage and reading it inward is correct. At 143px that turns its label into two ragged-left lines and its caption into three, beside a count that starts at its own left edge. Both start from their own left edge now, and the alignment comes back at 60rem with the corner placement that earns it. It is said in two properties, text-align on the block and justify-items on the stack of values, because the figure is a grid of four and the label and caption are paragraphs: written once they part company at the one width nobody re-reads, which is how the base came to carry a wide layout's alignment in the first place.
And aligning it was not enough, which the founder said in four words: “тут что-то ничего не поменялось на мобилку”. The alignment was right and the block still read wrong, because the defect underneath it was WIDTH. The pair is a count and a total and they are not the same object. The count is two short words, 86px at every phone width and never more; the total is a label, a figure and a caption, and its label alone wants 158px to stand on one line. Split down the middle at 390 they get 143 each - the count wastes 57 and the total is 57 short - so the label breaks in two, the caption in three, and four stacked fragments beside two words read as a block that failed. auto minmax(0, 1fr) gives each of them what it is: measured after, the label is one line at 360, 390 and 430, the caption is two, and the block is 92px tall against 127.
And the stack itself was centring every figure over its own word. .storystack is a two-consumer grid, the count's figure and the total's, and its base was justify-items: center: "14" sat centred above "subscriptions" and the amount above its caption. On a phone the pair reads left to right down one edge, and a figure floating over the middle of its label is the one thing in that pair that starts nowhere. The base is start now, and the wide layout takes it back where it earns it.
And on 2026-08-18 the phone stopped getting a column and got the stage
The founder asked one question and it had a yes in it. A screenshot of the section on a phone, and: “ты какую-то ерунду делаешь, посмотри, мы не можем сделать такую же идею, как и на десктопе?” We can, and the narrow arrangement of the stage was already in this file - three rows (head, swap, figures), the two figures as one sentence under the list, the reason card lying across the list's foot with --scardslot as the room it needs. It had never once run: a block in this file switched the whole choreography off under 60rem and handed the phone a plain column, and the orbit wrote a second, smaller act to give that column some motion.
The release was half applied, and the half that never applied is what the founder photographed. Its first line was min-height: 0 on .lp-story, written inside @container story (max-width: 59.99rem) - and .lp-story is the element that DECLARES the container named story. A container query is answered by an ancestor container, so a rule on the container itself, asked of its own container, can never match; it is dropped in silence. Every other line in that block addressed a descendant and landed. Measured at 390 x 844: the pin released to static with 1529px of content while the section kept the 320svh - 2701px - it had reserved for a stage that was no longer running. 1172px of nothing, one blank screen, at every phone width, for as long as the release existed.
What made the release look necessary was one number, and it was fixable. Its own comment said the figures were being pushed off the bottom of the stage at 360 x 740, and that was true: the promise and the heading share one cell, so laid out in the head row the head was 547px of a 780px stage and the list got 97. On a phone the promise fills the pin instead of taking a row in it - the orbit owns that rule and the measurements behind it - so the head row is the heading alone, 126px at 390, and the arithmetic becomes ordinary: 126 of head, 62 of figures, two gaps of 24, and 544 left for a list that is 460 tall.
Three cards in one cell must hand over, not cross-fade, and the first narrow screenshot caught it. The wide ranges overlap by six points and then six again, which is right when the cards are three boxes on three parts of the stage: one leaves on the left while the next arrives on the right, and the eye reads a relay. Narrow they are all in ONE cell, so those same six points drew two paragraphs through each other at half opacity - the smear this component already fixed once for the total, cured the same way. The windows are adjacent below 60rem, 25 to 43, 43 to 61, and sturn's own shape puts each card fully on screen through the cancellation it explains. Measured, 121 scroll stops at each of six phone sizes: the second most visible card is 0.000 everywhere.
And a card is a surface, so it needed ground to land on. At the desk it arrives over the page's own margin and its fade has never had to answer for itself; narrow it arrives over live rows, and a fill at half opacity is a window - four subscriptions were readable through “Clear, never judged” and it through them. The strip the card lies in takes a scrim of the page's own colour, drawn on .storyasides because that box is the card's cell and does not fade with the cards inside it, and it is exactly the room --scardslot already reserves. Not on the swap, which was the first attempt and was wrong by 43px: a mask there fades the bottom of the round window too, and the window is centred in the same box with its lower edge 101px above the floor, inside the 144 the scrim covers. The slot moved from the list to the pin so that a sibling could read it, which is a move of one line and not a change of value.
And the stage no longer dissolves, at any width, because the width was never the reason. The pin used to fade out over its last twelve per cent - six on the candidate - on the reading that the tail of the pin held a still frame and the fade could carry the hand-off to the next section. The founder killed it on the desk on 2026-08-15 (“в конце давай не делать исчезновение всего каскада, а просто продолжим скролл дальше”), but the answer went into the orbit's 60rem gate, so only the desk ever got it - and on 2026-08-18 they met the same fade on a phone: “пускай остается, а то пустой экран очень странно выглядит”. Measured before the fix: the pin's used animation-name was none at 1024, 1280, 1440 and 1920 and sout at 320, 360, 390, 414 and 768; at 390 x 844 the stage hit opacity 0 at scroll 1990 with the top of “How Tendd works” still 816px below the fold - 812px of scroll, a whole viewport, of empty page, and 992 at 768 x 1024. The premise was wrong, not the width: there is no still frame to cover, because past contain 100% the sticky releases and the stage travels up out of view on its own while the next section comes in behind it. The fade was not filling dead scroll, it was emptying live scroll. The pin now carries no animation at all - it is out of the timeline group and out of the reduced-motion kill list, both of which had nothing left to do - and the decision sits in the component that owns the pin instead of in one of its two arrangements. Measured after, at nine sizes from 320 to 1920: the pin's bottom edge and the next section's top edge are the same number at every scroll stop past the release, no gap and no overlap.
What is unchanged, measured rather than asserted. The desk: every element of the section fingerprinted at five widths from 1024 to 1920 by five scroll positions, 10 725 records, and every record that differs is a marquee chip, its mark or its price, differing in x alone - the marquee runs on a wall clock, and running the same code twice differs by more records than the change does. A short screen: @media (max-height: 42.5rem) still lets the stage go where four rows, a card and two figures genuinely do not fit, and it is a @media, so its own min-height: 0 reaches the section and works - 1px unused at 390 x 640 against 1172 before. Reduced motion: the same, and the promise is in the flow there, because the rule that lifts it out asks both releases as conditions rather than being switched off by whichever one remembers.
Where it comes from
Drawn by design/system/components/landing-story.css through ../system/index.css, the same entry point a product screen loads. It stands on the coloured landing and on wireframes/index.html, and on nothing else: the landing is the only public surface this product has. The founder's decision that made it is written into wireframes/index.html at the two blocks it changes and recorded as D-Hero in docs/decisions.md. Registered in docs/inventory.md and in _nav.js, in the organism group.
Two width facts changed on 2026-08-17, at stage 12, and one of them was the last unregistered width number in the product. The head of this section capped itself at 38rem, a literal in no register, written when the founder chose the round hero on 2026-08-15 and therefore after stage 10's register had closed. It is not registered, it is replaced: the same head is already given width: min(var(--container-column), 44cqw) in the stage form, so the cap is --container-column now. One box, one owner, 12px wider below the stage's own threshold and identical above it. And the lead inside it took its own measure, because the head looked measured and was not: ch is a unit of the element's own font, the head is 16px and the lead two type steps under it, so the same box that read 52ch for the heading read 80.2 characters for the sentence at a 720 window. A cap on a container measures the container.