Every container in Pindoba (cards, popovers, dialogs, sidebars, banners) is a Panel. A Panel doesn’t take a background color. It takes a position on a ramp, and the tokens for its fill, text, borders and hover states follow from that position.
Surfaces are named like terrain. Peak is the highest step and ground the lowest. Higher surfaces sit closer to the reader. The page itself stands mid-ladder on base: cards rise to peak above it, and wells, rails and selected rows sink toward ground below it.
Altitude here is structural depth, not shadow. Shadows are a separate axis (shadow="xs"…"xl") that you can add to any surface. A popover is a peak surface with a shadow. A sunken sidebar rail is a low step with no shadow.
Play with the props below. The readout shows the ramp and token names the Panel resolves to, and the contrast of its text measured on the painted fill.
secondary · primary
Text reads panel.text, so it follows this
surface.
Hover or press me to step the ramp.
A child panel resolves its tokens again from its own props.
<Panel background="surface.peak" emphasis="secondary" tone="primary" interactive shadow="sm" >
Each semantic group (neutral, primary, success, warning, danger) has a five-stop micro-ramp between shade 50 and shade 100:
surface.peak: shade 50 in light mode, 100 in dark. Cards, reading sheets, and popovers or menus with a shadow.surface.hill: a quarter of the way down. An intermediate raised tier.surface.base: the midpoint. The page floor (the default body background), and recessed regions inside a peak card.surface.valley: three quarters of the way down. Rails, insets and wells cut into the page.surface.ground: shade 100 in light mode, 50 in dark. The deepest wells on the page and window chrome. Not a floor: nothing can sink below it.transparent: no fill, so the page shows through. Hover still tints the page.An interactive panel moves down the ramp on hover (+1 step) and on press (+2 steps). From valley and ground there is no room to go lower, so they step back up instead. A transparent panel (a ghost button, a nav row, a list row) has no step of its own, so its states step off the surface underneath it. See The page floor.
The page stands on base, the middle of the ramp. It is the only step with room in both directions. Cards rise two steps to peak, and wells, columns and selected rows sink two steps toward ground. One neutral step (about 1.02:1) is below what the eye notices, so two steps is the smallest move that reads, and only base leaves two steps on both sides.
base. It’s the preset default: body is painted neutral.surface.base. Dashboards, boards, settings and inboxes put their content on peak cards above it.peak panel, not a peak page. An article, a docs page or an editor puts its text on one peak sheet standing on the base floor. The text gets the top step’s contrast, code blocks and callouts sink from it, and the chrome around it stays on the floor. These docs are built this way.ground is chrome, not a floor. It separates cards the most, but nothing can sit below it. Wells and kanban columns would have to rise instead, so keep it for window chrome and the deepest wells.Light and dark agree. Down the ramp is darker in both modes, so the same floor works in both.
A transparent panel’s hover, press and current (“selected”) fills are steps on the ramp, picked relative to the surface underneath. Every solid Panel publishes its step to the transparent panels inside it, and body publishes the page’s. So the same nav row reads correctly in a card, on the page and on a sunken rail:
| floor | hover | press | current | current + hover | current + press |
|---|---|---|---|---|---|
peak (inside cards) | base | valley | valley | ground | base |
hill | valley | ground | ground | valley | base |
base (the page) | peak | hill | ground | valley | hill |
valley (rails) | hill | base | peak | hill | base |
ground | base | valley | peak | hill | base |
On the page, hover lifts a row to the card surface and the current row sinks, so hovering never looks like a selection. Where there is no room below (valley, ground), every state rises.
A floor you paint yourself, without a Panel, has to say which step it is, or ghost rows on it step off the page’s base. Add data-surface-floor:
<aside
class={css({ bg: "neutral.surface.valley" })}
data-surface-floor="valley"
>
<!-- nav rows, ghost buttons… -->
</aside>
With a Panel (<Panel background="surface.valley">) this happens automatically.
Two props decide which ramp the background step is taken from:
emphasis picks the ramp. primary uses the solid accent band (<tone>.accent.surface.*), secondary uses the tinted surfaces (<tone>.surface.*), and tertiary uses the colorless neutral.surface.* but keeps the palette’s accent text.tone picks the palette: it sets colorPalette, so every colorPalette.* token below the panel follows it.The text follows the fill. A filled primary panel (solid step, not translucent) redirects panel.text.* to <tone>.text.contrast.*, which is solved against the accent band. Every other combination uses the palette’s own <tone>.text.*. On a coloured surface, write color: "panel.text" rather than a fixed group so your content flips with the panel.
emphasis="primary": the ramp is <tone>.accent.surface.*. When filled, panel.text is <tone>.text.contrast.emphasis="secondary": the ramp is <tone>.surface.*, and panel.text is <tone>.text.emphasis="tertiary": the ramp is neutral.surface.*. panel.text is <tone>.text, and the panel’s own text color is <tone>.text.accent.Panels stack. Each one resolves its tokens again from its own props, so a child can sit higher or lower than its parent. A common stack climbs from the floor to the card on top. With radius="inner", each child’s corner stays concentric with its parent’s.
Use tone="inherit" on a child to keep the parent’s palette, for example a neutral panel inside a success region.
translucent paints the chosen step at 35% opacity with a backdrop blur, so a panel over imagery reads as glass rather than a tinted slab. There is no single contrast ratio for glass, because it depends on what sits behind it. For that reason a translucent primary panel keeps the palette’s regular text, not the contrast text, and switches to text.bold on its two darkest steps.
surface.base, the preset’s body default. Don’t repaint it.surface.peak, emphasis="secondary", tone="neutral" (the defaults most components start from). Long-form text goes on a peak sheet, not a peak page.surface.valley, or surface.ground for the deepest. Declare data-surface-floor if you paint them by hand.surface.base; wells, code blocks and disabled fills on surface.valley.background="transparent". Their states follow the floor, so don’t hand-tint them.surface.peak plus a shadow. Elevation comes from the shadow, not from a different surface.emphasis="secondary" with a tone. Use emphasis="primary" only for a strong, filled call to action.emphasis="tertiary". The surface stays gray and the text carries the palette.translucent.For the full prop reference, including borders, active states, padding and radius, see Panel. The underlying color tokens are documented on Colors.