diff --git a/odoo-community-developers/index.html b/odoo-community-developers/index.html index d9adc56..d9d25b6 100644 --- a/odoo-community-developers/index.html +++ b/odoo-community-developers/index.html @@ -518,7 +518,15 @@ color: var(--brass-deep); } .dev-bio { - margin: 0; + /* The card reads as three blocks: the head/clients/focus block at the top, the + description centred in the free space, and the charts + ask row + links block + sitting on the card's floor. In a flex column an auto block margin absorbs the + card's free space equally above and below the description, which centres it + between the two blocks AND pushes the charts, the ask row and the links row + down — so the ask button lands at the same height on every card in the row. + No wrapper element, no markup change. The description is never clipped to + achieve this: it renders whatever the form gave it, up to 230 characters. */ + margin-block: auto; font-size: 0.9rem; line-height: 1.65; color: var(--ink-soft); @@ -562,6 +570,13 @@ align-items: center; flex-wrap: wrap; gap: 0.3rem; + /* The row is reserved, not filled: the script always keeps this container in the + DOM, even for a card with no link value at all, and this holds its line at the + height the 36 px hit areas already give it. An empty container is invisible but + occupies the same height as one full of logos, so the ask row above it sits at + the same height on every card — including a card with no logos. No empty anchor, + no empty icon, no placeholder text. */ + min-height: 36px; } .dev-links a { color: var(--midnight); @@ -2515,8 +2530,14 @@ var focus = capFocus(rec.Focus); if (focus) card.appendChild(liveEl("p", "dev-focus", focus)); - var bio = capDescription(rec.description); - if (bio) card.appendChild(liveEl("p", "dev-bio", bio)); + // The description container is ALWAYS in the DOM, like the links row further down: + // it is the element whose auto block margins centre it and push the bottom block + // onto the card's floor. A description is optional on the application form, and + // with no container the free space would have nothing to sit on — the charts, the + // ask row and the links row would float up off the floor and the ask button would + // miss the line every other card in the row is on. Reserved, not filled: an empty + // container reads as nothing, and a freelancer's own words are never clipped. + card.appendChild(liveEl("p", "dev-bio", capDescription(rec.description))); var rows = liveAxisRows(rec); if (rows.length) { @@ -2557,11 +2578,14 @@ // while ODOO_EXPERTS_QUOTE_WEBHOOK is set). The address itself is never rendered — // that is the whole point of the button. if (canAsk(rec)) card.setAttribute("data-relay", "1"); - if (links.length) { - var row = liveEl("p", "dev-links"); - links.forEach(function (a) { row.appendChild(a); }); - card.appendChild(row); - } + // The links row is ALWAYS in the DOM — even with nothing in it. An empty container + // costs exactly one reserved line (see .dev-links min-height) and keeps the ask row + // above it at the same height on every card; an ABSENT container would let the ask + // row drop a line on a card with no link value. Reserved, not filled: no empty + // anchor, no empty icon, no placeholder text. + var row = liveEl("p", "dev-links"); + links.forEach(function (a) { row.appendChild(a); }); + card.appendChild(row); if (!canAsk(rec)) { card.appendChild(liveEl("p", "dev-contact-none", "We have no way to pass your question to this freelancer."));