Landing steps
The "How Tendd works" section: a hairline rail, three ordinals at display size sitting on it, and under each one a piece of the real product doing the step the words describe.
What it is
It was built and rebuilt on the same day, and the second version is the one to read. The first was three cards, each with a 26px marker box on its top border and a washed grey well inside it. The founder looked and said two things: "так себе", and "ети цифирки маленькие ну просто кошмар". The number was the complaint; the card was the cause.
The ordinal is the section's spine, not a footnote on a card. A three-step sequence is the only structure this block has, and its structure was set at --type-meta inside a hairline box. It is now --type-step, the scale's own new step at 40px, set solid at weight 800 and tracked to -0.03em, and it is the first thing the eye lands on in each column. It is muted and not full ink, which is the one place the section pulls back: at 40px in primary ink it outweighed "How Tendd works" above it, and a step's number louder than the section's name is a hierarchy upside down. Big and quiet is the whole register of this language.
The card is gone. Three identical boxes each holding a picture-shaped rectangle is the shape every SaaS page has, and putting a surface inside a surface is a card inside a card. What carries the section now is the rail, the ordinals, the words straight on the canvas, and exactly one raised surface per column: a small piece of the real product, which is the only thing here that earns a shadow.
The rail is the section's sentence. A hairline runs at the ordinals' own mid-line, is interrupted by each numeral (which carries the canvas behind it and punches its own hole), and ends at its own column’s edge. Read across it is 1 ---- 2 ---- 3 at three equal lengths: each step carries its own rule, and the rule ends where the step’s block ends. It reached across the gap until 2026-08-17, when the founder marked the three ends on a screenshot: “сепаратор би сделал би макс длину до конца блока 1 2 3”. The old rule drew what the screenshot showed - the first two lines ran past their own column and stopped against the NEXT numeral, so each rail ended on somebody else’s step and only the third ended on its own. That is not one road, it is three lines of two lengths. The intro already promises "three steps, a few minutes"; the geometry now says it before the words do.
The three pictures are true, which is the taste decision that survived the rebuild. Step 1 is eight tiles: seven real provider marks and the plus, which is logo in its initials mode - the two doors of D2 and the paragraph's "400+ services", drawn rather than claimed. Step 2 is the list, with real names and real amounts from the canonical dataset, because grey skeleton bars are what a page draws when it has nothing to draw and this product has fourteen real rows. Step 3 is the same list one moment later: Netflix has gone quiet and wears the canonical Cancelled chip where its amount was, so the third picture is the second picture's consequence. No new strings, no invented prices, no icons, no petrol.
Anatomy
.lp-stepsits own grid and not.grid, and the reason changed with the rail on 2026-08-17. It used to be the bridge: the line crossed the gap, so the file had to know the gap, which auto-fill does not expose. What is left is the reason that was always the stronger one - these are steps, so the count is three or it is one, never the two-plus-one an auto-fill grid lands on when the container happens to fit two. A numbered sequence cannot wrap. One column below the point, three above it.lp-stepa column, not a card. It draws the rail as its own::beforeat 20px, half of the ordinal's 40px line box, so the line and the numeral share a middle.sordthe ordinal:--type-step, 800, -0.03em, tabular, muted, withwidth: max-contentand the page's own fill behind it so the background covers the digit and the air after it and not the whole rail.sdemothe one surface: border, radius, shadow,overflow: hidden(load-bearing, the rows run edge to edge), and a fixed 194px so the three headings under it stand on one line. Three rows of 64 plus two hairlines is 194, and step one's tile grid centres itself in the same box.drowa row of the picture: mark, name, then the amount or the chip. Its gap is 8 and not the real row's 16, which the Limits below measures.drow.is-offthe cancelled row. It goes quiet piece by piece and not behind one opacity, because an opacity takes the Cancelled chip down with it and the chip is the one thing in that row a person has to read.dgridstep one's tiles, four across atminmax(0, 56px)so they shrink together at the point instead of overflowing.dgrid .logothe one loop in the product that is not the brand, added 2026-08-17. Eight tiles on one nine second clock: the plus alone in the middle, pressed, seven services arriving 150ms apart, the plus docking in its slot, and back. The seven readdoorstilewith a delay each, the plus readsdoorspluswith none, so it owns the clock the seven are read against
Limits
The row's gap is 8, and a 40px band of window decided it. At the desktop point the column is 252px wide, and a row of a 32px mark + 16 + "Spotify Premium" + 16 + $11.99 inside 32px of padding wants 268: the name ellipsised from 900 to 939 and was clean from 940 up, which is exactly the "content stopped fitting and the query has not fired" band CLAUDE.md names. Probed at eight widths on both gaps; 8 is clean at every one. It is also the right unit for a miniature - this is a picture of a row at two thirds scale, not a row.
The rail is --line-container and it was --line-divider for one render. The divider role is --hairline-soft (#eef2f3) and the page's canvas is #eef3f4: one point apart, so the first cut of the rail rendered invisible and the three numerals floated with nothing between them. A divider is the line between two things inside a surface; this is a line on the canvas, which is the container role's job.
The entrance moves the content and never the structure. The rail and the three numerals are already there and stay still; the panel and the words rise into them on their own view() timelines, offset left to right. That is why the animation is on the pieces and not on the column: a staggered column carries its own rail with it, and three rails at three heights is a broken line for as long as the scroll lasts. cover and not entry for the reason landing-orbit.css measured. The curve is the same expo-out that file declares and cannot be read from here, so the literal carries its origin and the backlog carries the debt; stage 11 makes it one token. The reduced-motion kill is by name, because base.css's global guard zeroes durations and a scroll-driven animation has no duration to zero.
Step one moves now, and the movement is the block’s own sentence rather than decoration. Founder, 2026-08-17: “сначала кнопка по центру квадратик и плюсом нажимается и появляются аппликейшены по очереди ... короче ее надо сделать loop”. The heading says Connect or add and the paragraph says add your subscriptions yourself from 400+ services, and the still picture showed the RESULT of that and never the act: eight tiles already there, with the one control among them reading as an eighth logo. The cause is the press and the effect is the grid filling, which is the rule the four verbs on Motion were chosen under. Nine seconds, and 42 per cent of them are the still picture - from 2.97s to 6.75s all eight tiles stand exactly as the static markup draws them, which is what makes it a vignette rather than a carousel.
The two distances are computed from the grid and not measured off a screenshot. The plus is the eighth tile, column 4 of row 2, so the centre of a four by two grid is one and a half tiles and one and a half gaps left of it and half a tile and half a gap above. A percentage on translate resolves against the element’s own border box, so -150% is one and a half tiles whatever the track computes to, and this track shrinks. The gap reads --space-16. translate and scale and not transform, because those are the two properties logo.css already animates on this element for the lift. animation-fill-mode: backwards is load-bearing: a tile waits out up to 2.7s of delay, and with the default fill mode it would render visible for all of it and blink out when its animation began.
And the press answers on the panel, in the product's own accent. Founder, the same hour: “и при нажатии хочется на заднем фоне какую типа волну пустить в бренд цвет”. Until then the press was the one beat of the cycle with no consequence on screen - the plus went down and came back up, the grid began filling a fraction later, and the two read as two things that happened rather than as one causing the other. A wave leaves the plus at the frame it is pressed and the first service lands inside it: the front is still crossing at 2.34s and the first tile appears at 1.88s.
A FRONT and not a disc, which is what the word wave asks for. A flat fill scaling up is a colour swelling; an annulus scaling up is something travelling. It is one radial-gradient(closest-side, ...) on one pseudo-element, so both edges are feathered and the ring has no drawn outline to give it away as a circle. closest-side keeps the band's thickness relative to its own radius as it grows. It was rewritten twice in one evening and the third shape is the one that shipped. First one thin ring at 0.45, rejected as a sonar ping: “давай более нежную сделаем как будто капля упала в воду”. Then two thin rings, a drop's train of fronts, rejected as well: “мне не нравится эти волны, давай оставим одну и не как линию, а как заполненную, такую на всю карточку круг идет”. So the answer is not a front at all, it is a field. A ring, however soft, draws an EDGE, and an edge on a white panel is a shape a person tracks - at any frame the eye has an outline to follow, which is exactly why one ring reads as sonar and two read as ripples somebody counted. A filled circle has no edge to follow at this alpha; what crosses the panel is a change in the ground, and the only thing that reads is that something happened where the plus was pressed. 0.10 and not 0.26, and the area is the whole reason: the ring painted about a fifth of the circle and this paints all of it, so the same apparent weight over five times the area is roughly a fifth of the alpha, about six values per channel at the peak.
--ease-arrive is the one thing that survived both rewrites, and --ease-travel is back to one consumer. Travel is the even curve, written for two figures crossing a viewport, and it was chosen at shape one to stop the front covering its journey in a single flash. That is a reason about a FRONT crossing a distance, and a field has no front: the circle is thrown out by the press and the surface takes it back, quick at the centre and slow at the rim, which is expo-out exactly. The flash is answered by LENGTH instead - the circle opens from 1.08s to 3.06s where the first cut was done by 2.34, so it is still spreading, faintly, under the services as they land. The reversal is recorded rather than made quietly.
--bg-action and not a literal, and the ROLE is the argument rather than the colour. Petrol has four permitted jobs and the primary action is one of them; this ripple is the primary action of this picture answering a press, so it takes the fill role that answer already has. It is therefore a real colour in both themes with no new token and no pair to keep, which was checked with a theme control rather than assumed: the page ground moves rgb(238,243,244) to rgb(14,21,23) and the wave follows the role from #1c6a76 to #6bb0ba. DESIGN.md puts the BRAND's petrol in the chrome; this is not the brand appearing in content, it is the action's own ink doing the action's own job.
The origin needs no arithmetic and the cover needs no pixel. The grid is centred in the panel's content box and the panel's padding is equal on four sides, so the content box's centre IS the panel's centre and that is where the plus is: 50% / 50% and a self-centring translate. The circle is 140% of the panel's width rather than a size, because it has to beat the diagonal of a box that is 194 tall and anywhere from about 250 to 390 wide: radius 175 against a 173 diagonal at the narrow end, 273 against 213 at the wide one, and the margin grows with the box where a fixed pixel size would not. The overflow: hidden that was already there for the row corners does the second job for free - the circle is clipped to the panel's rounded rectangle, so what a person sees is colour crossing a surface rather than a disc growing on one.
The reduced-motion kill is by name here for a stronger reason than the entrance has. base.css zeroes the duration and forces one iteration, which lands an infinite loop on its LAST frame: the plus alone and seven services gone. That is not a stiller picture, it is a different one with the content removed. With the name killed the tiles render exactly as the markup writes them. Checked by cancelling the animations in the browser: all eight read opacity 1, no scale, no translate.
Step two browses itself, 2026-08-17. Founder: “одна подписка уходит вверх, а внизу добавляется другая, будто двигаем список, просматриваем наших подписок”. Same test as step one's loop: the heading says See it all, calmly, the paragraph is about a list a person scrolls, and the still picture showed three rows with no reason to believe there were more. Four rows is the minimum and it is arithmetic: a three-row window empties its bottom slot the moment the top row leaves, and the row that is leaving cannot also be the one arriving. The fourth is Notion at $8.00 from the canonical dataset, so no string is invented; with motion off it is clipped by the overflow: hidden this panel already declares, and the still picture is unchanged to the pixel.
align-content: start is the other half of that, and without it the still picture breaks. .sdemo centres its content, which is a no-op while three rows fill 194px exactly. Four rows are 260, so centred they would hang 33px off both ends and the top row would render cut in half. Start-aligned, the fourth simply falls off the bottom.
The rows come out of flow so one keyframes can drive all four, with the phase carried by a negative animation-delay of -6.75s, -4.5s and -2.25s: three quarters, a half and a quarter of the cycle, so at t=0 the four already stand at slots 0, 1, 2 and below, in markup order, with no fill mode and no first-cycle special case. The slots are -1, 64, 129, 194, and the -1 is load-bearing: every row carries a top hairline here, the first included, because any of them can end up mid-stack and a missing separator is the one thing this movement could break; the one-pixel lift clips exactly the hairline that would otherwise draw a double rule under the panel's own border. The wrap is instant and off-panel, from -66 to 194 in a thousandth of a percent, both outside a box that clips.
Guarded by no-preference rather than killed by name, unlike everything else on this page, because it changes LAYOUT and not only motion. Killing the animation alone would leave four absolutely positioned rows stacked on one point. Measured with the media feature asserted in the page: four rows position: static at 0, 65, 130 and 195, zero animations under the panel, the fourth off the bottom.
Step three cancels, in a loop. Founder: “по принципу как второй, но сложнее: нам надо показать, что мы отменяем нетфликс, он canceled становится и уходит вверх, и все двигается вверх, и так по кругу”. Step two's machinery plus one beat, and the beat is the point: the row standing at the TOP changes state before it leaves. The heading is Cancel and save and the still picture could only ever show the RESULT of that sentence, one row already cancelled with nothing to say when it became so. The loop shows the verb.
The two ends of a row share one slot and which is visible is the state. Every row carries both its amount and its Cancelled chip now. The chip is the one in flow and the amount lies over it, absolutely placed at the same right edge, and that way round keeps the row's geometry still: the chip is the wider of the two, so the space is reserved for the wider one and nothing reflows when the state changes. The other way round would put a chip with a background on top of the name.
The state is four properties on three elements and not one opacity on the row, which is this file's own oldest rule: an opacity on the row takes the Cancelled chip down with it, and the chip is the one thing in that row a person has to be able to read. The mark dims to 0.45, the name steps back to muted and to 400, the amount goes and the chip arrives, on --ease-state rather than --ease-arrive: nothing arrives here, a row already in front of you changes what it says about itself. The two ends hand over and do not cross-fade, and the first cut did cross and looked exactly like the bug it was not, "Cancelled" and "$11.99" both half painted in the same 70 pixels. Driven over the whole beat at 15ms steps, the highest simultaneous visibility of the two is 0.000.
What the reserved chip costs, measured rather than left out: one 16px band. Reserving the chip's width on every row takes 15px off the name column in step three, and at 900 to about 915 - the narrowest form of the three-column layout, immediately above the rung - "Spotify Premium" ellipsises. It is clean at every other width from 320 to 1920, and step two is clean everywhere. This is the same band the row's 8px gap was chosen for in the first place, it degrades through the ellipsis .name already declares, and the vignette is aria-hidden decoration; the alternative is a shorter label for that row, which is a Voice question and not a CSS one.
It took a token and it deleted a rule. --type-step is new in tokens.css between display and figure: a step's number and a subscription's amount are not one role, and this one is never money. And numbered-steps.css no longer draws .landing .lp-step .snum - that declaration held the landing's marker identical to the cancel guide's on the ground that two markers differing by four pixels is drift. They are no longer two editions of one thing, so the pair is closed rather than aligned, and the dead rule is deleted rather than left to rot.
The grey changed structure twice in one day and the decision is written where the rule demands. The wireframe is frozen; both changes are recorded as a founder's decision in a comment on the section in wireframes/index.html. The grey draws the same rail, the same ordinal and the same list in its own greyscale vocabulary. Its three-column point is 1120 and not the coloured layer's 900, and that is not a divergence: the grey landing renders inside the review stage beside the tree menu, so a 900 window gives this section 650px and a column 182. Measured, and written at the query.
What was rejected: icons, screenshots, and a fourth step. An icon set would be the first one in the product and would say less than the atoms already say; a real screenshot in a card repeats the mistake the hero corrected on 2026-08-14 ("все это смотрится очень плоско"); and the three steps are the sitemap's own three, not a layout choice this component may edit.
Where it comes from
Drawn by design/system/components/landing-steps.css through ../system/index.css. It stands on the landing and its grey twin. Ground: docs/decisions.md, D-Hero 2026-08-15. Registered in docs/inventory.md and in _nav.js, in the organism group. It hosts logo (including the initials mode for the plus tile), amount and chip, at sizes logo.css owns with the product's other seven host sizes. The marker it used to borrow from numbered steps is gone.