
Kamati: a fintech-style APEX app built from standard components and one stylesheet.
I recently built Kamati on Oracle APEX 26.1. It's a platform for running rotating savings committees (kamati, or ROSCAs). I wanted it to look like a modern fintech app: a violet gradient balance card, rounded white cards, icon tiles, filter chips and a bottom navigation bar on phones.
My first attempt built most screens as hand-written HTML inside Dynamic Content regions. It looked right, but it was the wrong approach for APEX. Nothing was editable in Page Designer, every list needed custom code, and I was rebuilding features APEX already gives you for free.
So I rebuilt every screen using only native APEX components and put the design on top with one CSS file. This post shows exactly how: which component each part of the screen uses, the CSS classes, the Page Designer settings, and the problems I ran into on the way.

The sign-in page. Two Static Content regions and native page items, nothing more.
The idea: native components plus one stylesheet
| Part of the UI | Native APEX component |
|---|
| Lists and summaries (Kamatis, members, payments, payouts, turns, owners) | Cards region |
| Search box and filter chips | Faceted Search region |
| Dashboard quick-action tiles | List region with the Cards list template |
| Phone bottom navigation | List region with the Links List template (on the Global Page) |
| Forms | Standard region with native page items and buttons |
| Greetings, login illustration | Static Content region |
Each region gets one or two CSS classes, all prefixed km-, in Appearance > CSS Classes. The stylesheet then restyles APEX's own markup (a-CardView, t-Cards, t-LinksList, a-FS, t-Form) only for regions that carry those classes.

Four steps from a plain region to the finished look.
What you get from this:
- Every page is still 100% editable in Page Designer.
- The design stays consistent, because every list uses the same few classes.
- APEX keeps doing the heavy lifting: pagination, session state, checksums, accessibility and lazy loading.

The owner dashboard. Every block on this page is a Cards or List region.
Step 1: Load the stylesheet and the font
- Shared Components > Static Application Files > Create File: upload
kamati.css. - Shared Components > User Interface Attributes > CSS > File URLs:
User Interface Attributes > CSS > File URLs
https://fonts.googleapis.com/css2?family=Mulish:wght@400;600;700;800&display=swap
#APP_FILES#kamati.css
That's the only global setup. Everything else is a class on a region.
Step 2: Design tokens and Universal Theme variables
All colours, radii and shadows are CSS variables, so the whole look can change in one place. Universal Theme also exposes its own variables, so I point those at my tokens too. That's how checkboxes, radio buttons, focus rings and primary buttons turn violet without overriding dozens of selectors.
Design tokens
:root {
--km-primary: #7c5cfc;
--km-primary-600: #6a4af0;
--km-primary-50: #f3f0ff;
--km-ink: #1e1b3a;
--km-muted: #8a88a6;
--km-line: #eeebfa;
--km-bg: linear-gradient(135deg, #fdf2fb 0%, #f4f1ff 45%, #eaf4ff 100%);
--km-grad: linear-gradient(135deg, #9a7dff 0%, #7c5cfc 55%, #6a4af0 100%);
--km-radius: 18px;
--km-shadow: 0 10px 30px rgba(124, 92, 252, .08), 0 2px 6px rgba(30, 27, 58, .04);
--km-font: "Mulish", "Segoe UI", system-ui, sans-serif;
/* Universal Theme variables */
--a-base-font-family: var(--km-font);
--ut-palette-primary: var(--km-primary);
--a-checkbox-checked-background-color: var(--km-primary);
--a-field-input-border-radius: 12px;
--a-field-input-focus-border-color: var(--km-primary);
}
The page shell is a soft pink-to-blue gradient behind everything, with a translucent white header:
Page shell
html, body.t-PageBody { background-image: var(--km-bg) !important; background-attachment: fixed !important; }
.t-PageBody, .t-Body, .t-Body-main, .t-Body-content, .t-Body-contentInner { background: transparent !important; }
.t-Header { background: rgba(255, 255, 255, .9) !important; backdrop-filter: blur(10px); box-shadow: 0 1px 0 var(--km-line); }
And the current item in the side navigation becomes a violet gradient pill:
Side navigation
.t-TreeNav .a-TreeView-node--topLevel > .a-TreeView-content.is-current {
background: var(--km-grad) !important; color: #fff !important; }
Step 3: Cards regions do almost everything
The Cards region is the workhorse. One SQL query, a few column mappings and one or two CSS classes give me every list in the app.
3.1 A badge helper in the database
Most cards show a coloured status badge, so I return both the label and a CSS class from SQL with a tiny helper package:
km_ui_pkg (excerpt)
function status_label (p_status varchar2) return varchar2 is
begin
return initcap(replace(p_status, '_', ' ')); -- 'PAYMENT_OVERDUE' -> 'Payment Overdue'
end;
function status_class (p_status varchar2) return varchar2 is
begin
return 'km-badge km-badge--' || case
when p_status in ('PAID', 'ACTIVE', 'APPROVED') then 'green'
when p_status in ('COMPLETED', 'RECEIVED') then 'violet'
when p_status in ('PARTIAL', 'AWAITING_PAYMENT') then 'amber'
when p_status in ('PENDING', 'SCHEDULED') then 'blue'
when p_status in ('LATE', 'REJECTED', 'SUSPENDED') then 'red'
else 'grey' end;
end;
Badge styles
.km-badge { display: inline-block; padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 800; }
.km-badge--green { background: #e9f9ef; color: #16a34a; }
.km-badge--red { background: #ffe9ee; color: #e11d48; }
/* ...amber, blue, violet, grey */
3.2 List-style cards (rows inside one white card)
I use this for due and late payments, payouts, turns and installments.

The Payments page: a Faceted Search on top, list-style Cards below.
Cards region source (SQL)
select payment_id,
first_name || ' ' || last_name as full_name,
kamati_name || ' · due ' || to_char(due_date, 'DD Mon YYYY') as detail,
km_ui_pkg.money(amount_outstanding) as amount,
km_ui_pkg.status_label(status) as status_label,
km_ui_pkg.status_class(status) as status_class
from v_km_pending_payment
order by due_date
Page Designer, Cards region > Attributes:
| Setting | Value |
|---|
| Appearance > Layout | Grid, 1 column |
| Appearance > CSS Classes | km-cards km-cards--list |
| Card > CSS Classes | km-card--out (amount in red) or km-card--in (green) |
| Title > Column | FULL_NAME |
| Subtitle > Column | DETAIL |
| Body > Column | AMOUNT |
| Icon > Source | Initials, column FULL_NAME |
| Badge > Column | STATUS_LABEL |
| Badge > CSS Classes | &STATUS_CLASS. |
| Actions > Full Card | Link to the Record payment page with P42_PAYMENT_ID = &PAYMENT_ID. |
APEX renders each card as a vertical flex box (.a-CardView). For a list row I turn it into a horizontal row: icon, title and subtitle, badge, then the amount on the right.
List-style cards
.km-cards--list { gap: 0 !important; padding: 6px !important; background: #fff;
border-radius: var(--km-radius); box-shadow: var(--km-shadow); }
.km-cards--list .a-CardView { flex-direction: row !important; align-items: center; box-shadow: none; }
.km-cards--list .a-CardView-item + .a-CardView-item { border-top: 1px solid var(--km-line); }
.km-cards--list .a-CardView-header { flex: 1; min-width: 0; padding: 12px 10px; align-items: center; }
.km-cards--list .a-CardView-headerBody { flex: 1; min-width: 0; }
.km-cards--list .a-CardView-body { flex: 0 0 auto !important; padding: 0 12px 0 0; } /* don't let the amount grow */
.km-cards--list .a-CardView-mainContent { font-weight: 800; text-align: right; white-space: nowrap; }
.km-card--out .a-CardView-mainContent { color: #e11d48; }
.km-card--in .a-CardView-mainContent { color: #16a34a; }
.km-cards .a-CardView-initials { width: 40px; height: 40px; border-radius: 13px;
background: var(--km-primary-50) !important; color: var(--km-primary-600) !important; font-weight: 800; }
GOTCHA
Switch the flex direction first
My first version used align-items: center without flex-direction: row. In a column flex box that centres everything horizontally, so the rows looked like narrow centred cards. Always change the direction first, then the alignment.
A per-row button instead of a full-card link. On the Kamati detail page only unpaid installments get a Collect button. That's a card action of type Button with a server-side condition checked for each row:
- Action type: Button, label
Collect, Hot - Server-side Condition: PL/SQL Expression
:STATUS in ('PENDING', 'PARTIAL', 'LATE') - Execute for each row: Yes
3.3 Grid cards with Advanced Formatting
The Kamatis page uses a 3-column grid. The body uses Advanced Formatting (an HTML Expression) with substitution strings and template directives. It's still a native card, just with a richer layout.

Grid cards: meta row in the body, progress bar in the secondary body.
Body > Advanced Formatting > HTML Expression
<div class="km-meta">
<span><span class="fa fa-users"></span> &MEMBERS.</span>
<span><span class="fa fa-random"></span> &BALLOT.</span>
<span><span class="fa fa-trophy"></span> &POT.</span>
</div>
Secondary Body > HTML Expression (progress bar)
<div class="km-foot"><span>&PROGRESS_LABEL.</span><span>&PROGRESS_NOTE.</span></div>
<div class="km-progress"><span style="width:&PCT.%"></span></div>
Template directives such as {if COLUMN/} ... {endif/} keep it declarative. For example, the members page only shows the "App" pill when the member has a login:
Template directives
<span class="fa fa-phone"></span> &PHONE_NUMBER.{if CITY/} · &CITY.{endif/}
{if HAS_LOGIN/}<span class="km-pill km-pill--ok">App</span>{endif/}
TipAPEX escapes &COLUMN. substitutions, so the HTML expression stays safe.
Grid cards
.km-cards .a-CardView { border: 0; border-radius: 18px; background: #fff; box-shadow: var(--km-shadow); transition: transform .15s ease; }
.km-cards .a-CardView.has-actions--full:hover { transform: translateY(-2px); }
.km-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--km-muted); font-weight: 600; }
.km-progress { height: 8px; border-radius: 99px; background: var(--km-primary-50); overflow: hidden; }
.km-progress > span { display: block; height: 100%; background: var(--km-grad); }
3.4 The gradient balance card
The purple "Collected this cycle" card is just a single-row Cards region with the classes km-cards km-cards--balance:
Balance card source (SQL)
select 'Collected this cycle' as label,
km_ui_pkg.money(sum(collected_total)) as amount,
'of ' || km_ui_pkg.money(sum(expected_total)) || ' expected' as meta,
round(sum(collected_total) / nullif(sum(expected_total), 0) * 100) as pct
from v_km_kamati_summary
where status = 'ACTIVE'
Body > HTML Expression
<span class="km-balance__amount">&AMOUNT.</span>
<span class="km-balance__meta">&META.</span>
{if PCT/}<div class="km-progress km-progress--light"><span style="width:&PCT.%"></span></div>{endif/}
<svg class="km-balance__wave" viewBox="0 0 120 44"><path d="M4 34 C 22 34, 26 10, 46 12 S 72 38, 92 22 S 112 6, 116 8"/></svg>
Balance card
.km-cards--balance .a-CardView { position: relative; overflow: hidden; background: var(--km-grad) !important;
color: #fff; border-radius: 22px; box-shadow: 0 18px 40px rgba(106, 74, 240, .28); }
.km-balance__amount { font-size: 32px; font-weight: 800; }
.km-balance__wave { position: absolute; right: 22px; top: 22px; width: 96px; height: 40px; }
.km-balance__wave path { fill: none; stroke: #5eead4; stroke-width: 4; stroke-linecap: round; }
3.5 More card variants, same idea
| Class | Used for |
|---|
km-cards--stats | 4 small stat tiles (2 per row on phones) |
km-cards--hero | Detail page header with a facts grid (km-facts) and hints (km-hint) |
km-cards--banner | Gradient "Rabia receives Rs 10,000 · Pay out" banner with a card button |
km-cards--status | Centred "Registration received" status card |
km-cards--turns | Turn numbers as round gradient avatars (Initials icon) |
km-cards--people | Member and owner cards |

The Kamati detail page: hero, banner and list cards, all from Cards regions.
Highlighting a row from SQL works through Card > CSS Classes = &CARD_CLASS.. In the member's turn schedule, the member's own row returns km-card--me and gets a violet background.
Step 4: Quick-action tiles from a native List
The four tiles (New Kamati, Add Member, Collect Payment, Payouts) are a shared List shown with a List region:
| Setting | Value |
|---|
| Region type | List |
| List Template | Cards |
| Template Options | Compact, Display Icons, 4 Columns |
| Appearance > CSS Classes | km-tiles |
| Each list entry | Label, target page, icon (fa-plus-circle, fa-user-plus, ...), Authorization Scheme |
Because each entry has an authorization scheme, a collector only sees the tiles they're allowed to use, with no extra code. The pastel icon colours come from nth-child:
Quick-action tiles
.km-tiles .t-Cards { display: grid !important; gap: 12px; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 720px) { .km-tiles .t-Cards { grid-template-columns: repeat(4, 1fr); } }
.km-tiles .t-Card { border-radius: 18px; overflow: hidden; background: #fff; box-shadow: var(--km-shadow); }
.km-tiles .t-Card-wrap { flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 104px; }
.km-tiles .t-Card-icon { position: static; width: 46px; height: 46px; border-radius: 14px; display: grid; place-items: center; }
.km-tiles .t-Card-subtitle, .km-tiles .t-Card-body, .km-tiles .t-Card-colorFill { display: none; }
.km-tiles .t-Cards-item:nth-child(4n+1) .t-Card-icon { background: #f3f0ff; color: #7c5cfc; }
.km-tiles .t-Cards-item:nth-child(4n+2) .t-Card-icon { background: #ffe8f3; color: #db2777; }
.km-tiles .t-Cards-item:nth-child(4n+3) .t-Card-icon { background: #fff4e0; color: #d97706; }
.km-tiles .t-Cards-item:nth-child(4n+4) .t-Card-icon { background: #e3f8f5; color: #0d9488; }
Step 5: Faceted Search as a search bar and filter chips
Members, payments and owners all use a Faceted Search region whose Filtered Region is the Cards region.
- Facet 1: Search (type Search, source columns
FULL_NAME, PHONE_NUMBER, CNIC, CITY), label hidden - Facet 2: Checkbox Group on
STATUS (static LOV), with counts - Region CSS Classes:
km-search
By default facets render as a vertical checkbox list. A few rules turn each facet into one row: an uppercase label, then pill-shaped chips with counts. Selected chips turn violet.
Faceted Search as chips
.km-search input[type="search"] { border-radius: 14px !important; min-height: 44px; background: #fff !important; box-shadow: var(--km-shadow); }
.km-search .a-FS-control { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; border: 0 !important; background: transparent; }
.km-search .a-FS-label { font-size: 12px; font-weight: 800; color: var(--km-muted); text-transform: uppercase; }
.km-search .a-FS-body { flex: 1 1 auto; min-width: 0; }
.km-search .apex-item-checkbox { display: flex !important; flex-direction: row !important; flex-wrap: wrap; gap: 8px; }
.km-search .apex-item-option label { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px;
border-radius: 999px; background: #fff; box-shadow: var(--km-shadow); font-weight: 700; cursor: pointer; }
.km-search .apex-item-option label::before,
.km-search .apex-item-option label::after { display: none !important; } /* hide the checkbox drawing */
.km-search .apex-item-option[aria-selected="true"] label { background: var(--km-grad); color: #fff; }
.km-search .apex-item-option.is-disabled { display: none; } /* hide zero-count values */
.km-search .apex-item-option-badge { padding: 0 7px; border-radius: 999px; background: #f3f0ff; color: #6a4af0; font-size: 11px; }

Two facets, each on one row: account state and plan, with counts.
Step 6: Forms that feel like an app
Forms are plain Standard regions with native items. Four settings did most of the work:
- Region Template Options > Stretch Form Items, plus the item template option Stretch (
t-Form-fieldContainer--stretchInputs). Without this, fields are short fixed-width boxes. - Label template "Optional - Above" / "Required - Above". Labels above fields read much better on phones.
- Hide the region title (Template Options > Header: Hidden) when the page heading already names the form.
- CSS class
km-form, which moves the buttons under the fields. Universal Theme puts the Next and Previous positions at the top of the region.
Buttons under the fields
.km-form .t-Region-bodyWrap { display: flex; flex-direction: column; }
.km-form .t-Region-body { order: 1; }
.km-form .t-Region-buttons--top { order: 2; padding: 14px 16px 18px; border-top: 1px solid var(--km-line); }
@media (max-width: 640px) { /* full-width, primary button first on phones */
.km-form .t-Region-buttons--top { display: flex; flex-direction: column-reverse; gap: 10px; }
.km-form .t-Region-buttons--top .t-Button { width: 100%; justify-content: center; min-height: 46px; }
}
Inputs get the soft look, and the attached buttons (calendar, show password) match the field:
Inputs
.t-Form-inputContainer input.text_field, .t-Form-inputContainer select, .t-Form-inputContainer textarea {
border-radius: 12px; border-color: #e4e0f7; background: #fbfaff; min-height: 42px; }
.t-Form-inputContainer :is(input, select, textarea):focus { border-color: var(--km-primary); box-shadow: 0 0 0 3px rgba(124, 92, 252, .18); }
.t-Form-itemWrapper > .a-Button { border-radius: 0 12px 12px 0 !important; background: #f3f0ff; color: #6a4af0; }
.t-Form-itemWrapper > input:has(+ .a-Button) { border-top-right-radius: 0 !important; border-bottom-right-radius: 0 !important; }
GOTCHA
:not(:last-child) was too greedy
The last rule first used input:not(:last-child). That also matched fields with an icon after them, so the mobile-number field lost its rounded corners. :has(+ .a-Button) targets only fields that really have an attached button.

The New Kamati form: stretched items, labels above, buttons at the bottom.
Step 7: A bottom navigation bar on phones
On phones the side menu is replaced by a floating bottom bar. It's also a native List region:
- Created on the Global Page (page 0) so it appears everywhere
- List Template Links List, Template Option Show Icons
- CSS Classes
km-bnav - Server-side Condition: user is signed in and the page isn't the login page
- Each list entry has an Authorization Scheme: owners see Home, Kamatis, Members, Payments and Payouts, members see Home and Password
- "List Entry Current for Pages", for example
20,21,22 for Kamatis, so the active tab is highlighted
Bottom navigation
.km-bnav { display: none; }
@media (max-width: 640px) {
.km-bnav { position: fixed; z-index: 900; left: 12px; right: 12px; bottom: 12px; display: block;
padding: 8px 6px calc(8px + env(safe-area-inset-bottom)); border-radius: 22px;
background: rgba(255, 255, 255, .96); box-shadow: 0 10px 30px rgba(30, 27, 58, .14); }
.km-bnav .t-LinksList { display: flex; justify-content: space-around; }
.km-bnav .t-LinksList-link { flex-direction: column; align-items: center; gap: 3px; font-size: 10.5px; font-weight: 700; }
.km-bnav .t-LinksList-item.is-current > .t-LinksList-link { background: var(--km-primary); color: #fff; border-radius: 14px; }
.t-Body-content { padding-bottom: 96px; } /* room for the bar */
.t-Header-controls .t-Button--headerTree { display: none; } /* no hamburger needed */
}

The same dashboard on a phone, with the bottom bar from page 0.
Step 8: The login page, with demo buttons

On a phone the hero panel hides and the card gets its own brand line.
Page settings: template Minimal (No Navigation), Page CSS Classes km-auth-page.
| Region | Type | Grid | CSS Classes |
|---|
| Hero (brand, headline, phone mock-up, feature pills) | Static Content | 6 columns | km-auth-col km-auth-col--hero |
| Sign in (heading, items, buttons) | Static Content, Standard template, header hidden | 6 columns, new column | km-form km-auth-card km-auth-col |
In the sign-in region, Item Display Position = Below Content, so the heading HTML (the region source) appears above the fields.
Layout CSS: no header bar, a centred 1120px container, and the two columns stretched to the same height:
Sign-in layout
.km-auth-page .t-Header { display: none; }
.km-auth-page .t-Body-contentInner { max-width: 1120px; min-height: 100vh; margin: 0 auto;
display: flex; flex-direction: column; justify-content: center; padding: 32px 24px !important; }
.km-auth-page .t-Body-contentInner .row { align-items: stretch; }
.km-auth-page .t-Body-contentInner .col { display: flex; flex-direction: column; }
.km-auth-col { flex: 1 1 auto; }
@media (max-width: 1000px) { .km-phone { display: none; } } /* tablet: no mock-up */
@media (max-width: 640px) { .km-auth-col--hero { display: none; } /* phone: card only */
.km-auth-card__brand { display: flex; } }
One-click demo buttons
Two native buttons, Demo owner and Demo member:
| Setting | Value |
|---|
| Button Position | Previous |
| Behavior > Execute Validations | No (the empty username and password fields mustn't block them) |
| Build Option | Demo Logins |
| CSS Classes | km-demo-btn |
Each button has a page process (Server-side Condition: When Button Pressed) that signs in with a demo account:
Process: demo owner login
begin
apex_authentication.login(p_username => '03001110001', p_password => '<demo password>');
end;
The normal Login process gets the condition :REQUEST not in ('DEMO_OWNER', 'DEMO_MEMBER').
TipThe Build Option is the important part. Before going live, set Shared Components > Build Options > Demo Logins to Exclude, and the buttons and their processes disappear from the app. You don't delete or change any code.
The card stacks a full-width Sign in button, an "or explore with demo data" divider (pure CSS), then the two demo buttons side by side:
Sign-in card buttons
.km-auth-card .t-Region-buttons--top { display: flex !important; flex-direction: column !important; gap: 16px; border-top: 0 !important; }
.km-auth-card .t-Region-buttons-right { order: 1; width: 100%; }
.km-auth-card .t-Region-buttons-right .t-Button { width: 100%; justify-content: center; min-height: 50px; }
.km-auth-card .t-Region-buttons-left { order: 2; display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px; }
.km-auth-card .t-Region-buttons-left:not(:has(.t-Button)) { display: none !important; } /* build option excluded */
.km-auth-card .t-Region-buttons-left::before { content: "or explore with demo data"; grid-column: 1 / -1; text-align: center;
font-size: 12px; font-weight: 700; color: var(--km-muted);
background: linear-gradient(var(--km-line), var(--km-line)) left center / 26% 1px no-repeat,
linear-gradient(var(--km-line), var(--km-line)) right center / 26% 1px no-repeat; }
.km-auth-card .km-demo-btn { min-height: 46px; border-radius: 14px; justify-content: center;
background: #f3f0ff !important; border: 1px dashed #cfc2ff !important; color: #6a4af0 !important; font-weight: 800; }

The member portal: same classes, a different SQL query, a different audience.
CSS class reference
| Class | Put it on | What it does |
|---|
km-cards | Cards region > Appearance > CSS Classes | Base card look: white, rounded, soft shadow, hover lift |
km-cards--list | Cards region (1 column) | Rows inside one white card, amount and badge on the right |
km-cards--grid | Cards region (2 or 3 columns) | Grid cards with meta row and progress bar |
km-cards--people | Cards region | Muted body text for member and owner cards |
km-cards--balance | Single-row Cards region | Purple gradient balance card with wave |
km-cards--stats | Cards region (4 columns) | Small stat tiles, 2 per row on phones |
km-cards--hero | Single-row Cards region | Detail header with a km-facts grid |
km-cards--banner | Single-row Cards region | Gradient banner with a white action button |
km-cards--status | Single-row Cards region | Centred status message with a big icon |
km-cards--turns | Cards region | Turn numbers as round gradient avatars |
km-cards--context | Cards region | Soft violet "what you're editing" strip above a form |
km-card--out / km-card--in | Card > CSS Classes | Amount in red / green |
km-card--me | Card > CSS Classes (&CARD_CLASS.) | Highlight the signed-in user's row |
km-stat--alert | Card > CSS Classes | Red stat value |
km-badge km-badge--{colour} | Badge CSS Classes (from SQL) | Status pills: green, amber, red, blue, violet, grey |
km-pill, km-pill--ok | Inside HTML expressions | Small grey / green pills |
km-meta, km-foot, km-progress | Inside HTML expressions | Meta row, progress label row, progress bar |
km-facts, km-hint, km-hint--warn | Inside HTML expressions | Facts grid, info and warning callouts |
km-myturn, km-turn | Inside HTML expressions | "Your turn" block with a number bubble |
km-tiles | List region (Cards template) | Quick-action tiles with pastel icons |
km-search | Faceted Search region | Rounded search bar and filter chips |
km-form | Standard region with items | Buttons under the fields, full width on phones |
km-narrow | Form region | Max width 680px for short forms |
km-section | Standard region (no UI) | Region title as a section heading, transparent body |
km-actions, km-actions--center | Buttons Container region | Chrome-less button row (centred) |
km-bnav | List region on page 0 | Floating bottom navigation on phones |
km-page-head, km-greet | Static Content HTML | Page title and subtitle / greeting |
km-auth-page | Page > CSS Classes | Full-height sign-in layout, no header |
km-auth-col, km-auth-col--hero | Login regions | Equal-height columns, hero hidden on phones |
km-auth-card | Sign-in form region | Big card, stacked sign-in and demo buttons |
km-demo-btn | Demo buttons > CSS Classes | Dashed violet demo buttons |
Gotchas I hit (so you don't have to)
- Use native components from the start. Hand-written HTML in Dynamic Content regions looks fine on day one, but you lose Page Designer, Faceted Search, pagination and checksummed links.
- Cards are vertical flex boxes. For row layouts set
flex-direction: row first, then the alignment. - Stop the card body from growing (
flex: 0 0 auto), or the badge drifts to the middle of the row. - Badge classes can come from SQL: set Badge CSS Classes to
&STATUS_CLASS.. - Template directives (
{if COL/}...{endif/}) keep HTML expressions declarative and tidy. - Buttons that bypass required fields (demo login, cancel-style actions): set Execute Validations = No.
- Demo-only features belong behind a Build Option, so production is one switch away.
- Links inside Static Content: with friendly URLs, a relative
f?p=... link returns 404. Use the page alias (href="register") or let APEX generate the link (list entry, button, card action). - Universal Theme variables (
--ut-palette-primary, --a-checkbox-checked-background-color, --a-field-input-*) are the cleanest way to recolour native widgets. - Test on real screen sizes. I used a small Puppeteer script with headless Edge to sign in as each role, screenshot every page at desktop and phone widths, and check for errors after every change. Most of the layout bugs above were caught that way, not by eye.
Is this approach worth it?
What works well
- ✔Every page stays editable in Page Designer.
- ✔Faceted Search, pagination, session state and checksums all keep working.
- ✔One file controls the whole look. Change a token, the app follows.
- ✔Authorization schemes on list entries hide tiles and tabs with no extra code.
- ✔A new developer only needs to learn a handful of km- classes.
What to keep in mind
- ✘The CSS targets Universal Theme class names, so check it after an APEX upgrade.
- ✘Some rules need !important to beat theme styles.
- ✘Advanced Formatting HTML is still HTML. Keep it short.
- ✘:has() needs a modern browser.
- ✘Test at phone and desktop widths after every change.
Summary
- Build every screen from native components: Cards, Faceted Search, Lists, Static Content and form items.
- Load one stylesheet from Static Application Files and give each region a km- style class.
- Drive colours through CSS variables, and point the Universal Theme variables at them.
- Return badge and card classes from SQL with &STATUS_CLASS. and &CARD_CLASS.
- Use Advanced Formatting and template directives for richer cards without leaving the Cards region.
- Put the phone bottom navigation on page 0 as a Links List.
- Keep demo logins behind a Build Option.
Every screen in Kamati is a standard APEX component, and the whole look lives in one stylesheet of about 400 lines. Anyone who knows APEX can open Page Designer and change a query, add a facet or reorder the tiles without touching HTML, and the design keeps working.
If you're building a mobile-friendly APEX app, try this pattern: native components first, one km--style class per region, and a single stylesheet on top. The complete stylesheet is below. Feel free to adapt it for your own apps, and if you get stuck, leave a comment.
Appendix: the complete stylesheet (kamati.css)
kamati.css
/* Kamati "Appwind" look for apps 100 and 200, applied on top of native APEX components:
Universal Theme pages/regions/forms, Cards regions (a-CardView), List regions (t-Cards, t-LinksList),
Faceted Search. Pages opt in through region/component CSS classes (km-*), so everything stays
editable in Page Designer. */
:root {
--km-primary: #7c5cfc;
--km-primary-600: #6a4af0;
--km-primary-50: #f3f0ff;
--km-ink: #1e1b3a;
--km-muted: #8a88a6;
--km-line: #eeebfa;
--km-card: #ffffff;
--km-field: #fbfaff;
--km-field-border: #e4e0f7;
--km-bg: linear-gradient(135deg, #fdf2fb 0%, #f4f1ff 45%, #eaf4ff 100%);
--km-grad: linear-gradient(135deg, #9a7dff 0%, #7c5cfc 55%, #6a4af0 100%);
--km-radius: 18px;
--km-shadow: 0 10px 30px rgba(124, 92, 252, .08), 0 2px 6px rgba(30, 27, 58, .04);
--km-shadow-hover: 0 16px 36px rgba(124, 92, 252, .16);
--km-shadow-lg: 0 18px 40px rgba(106, 74, 240, .28);
--km-green: #16a34a; --km-green-bg: #e9f9ef;
--km-amber: #d97706; --km-amber-bg: #fff4e0;
--km-red: #e11d48; --km-red-bg: #ffe9ee;
--km-blue: #4f46e5; --km-blue-bg: #eef0ff;
--km-teal: #0d9488; --km-teal-bg: #e3f8f5;
--km-pink: #db2777; --km-pink-bg: #ffe8f3;
--km-grey: #6b7280; --km-grey-bg: #f1f1f6;
--km-font: "Mulish", "Segoe UI", system-ui, -apple-system, sans-serif;
/* Universal Theme variables */
--a-base-font-family: var(--km-font);
--ut-palette-primary: var(--km-primary);
--ut-palette-primary-contrast: #fff;
--ut-palette-primary-shade: var(--km-primary-50);
--ut-palette-primary-text: var(--km-primary-600);
--a-checkbox-checked-background-color: var(--km-primary);
--a-checkbox-checked-text-color: #fff;
--a-field-input-border-radius: 12px;
--a-field-input-border-color: var(--km-field-border);
--a-field-input-focus-border-color: var(--km-primary);
--ut-field-input-focus-icon-color: var(--km-primary);
}
/* ===== Page shell ============================================================================ */
html, body.t-PageBody { background: #f6f4ff !important; background-image: var(--km-bg) !important; background-attachment: fixed !important; }
body { font-family: var(--km-font); color: var(--km-ink); }
.t-PageBody, .t-Body, .t-Body-main, .t-Body-content, .t-Body-contentInner, .t-Body-title { background: transparent !important; }
.t-Footer { display: none; }
.t-Body-contentInner { max-width: 1240px; }
.t-Header { --ut-header-background-color: #fff; --ut-header-text-color: var(--km-ink);
background: rgba(255, 255, 255, .9) !important; color: var(--km-ink) !important; backdrop-filter: blur(10px); box-shadow: 0 1px 0 var(--km-line); }
.t-Header .t-Icon, .t-Header .t-Button-label, .t-Header .apex-logo-text { color: var(--km-ink) !important; }
.t-Header-logo-link { font-weight: 800; font-size: 18px; }
.t-Header-logo-link::before { content: "K"; display: inline-grid; place-items: center; width: 30px; height: 30px; margin-right: 8px;
border-radius: 10px 10px 10px 3px; background: var(--km-grad); color: #fff; font-size: 15px; vertical-align: middle; }
/* Side navigation (desktop) */
.t-Body-nav, .t-TreeNav { background: transparent !important; border-right: 0; }
.t-TreeNav .a-TreeView-content { margin: 2px 8px; border-radius: 12px; color: var(--km-ink); font-weight: 700; }
.t-TreeNav .a-TreeView-content:hover { background: var(--km-primary-50); }
.t-TreeNav .a-TreeView-node--topLevel > .a-TreeView-content .fa { color: var(--km-primary); }
.t-TreeNav .a-TreeView-node--topLevel > .a-TreeView-content.is-current--top,
.t-TreeNav .a-TreeView-node--topLevel > .a-TreeView-content.is-current { background: var(--km-grad) !important; color: #fff !important; box-shadow: 0 8px 18px rgba(106, 74, 240, .25); }
.t-TreeNav .a-TreeView-content.is-current .fa, .t-TreeNav .a-TreeView-content.is-current--top .fa { color: #fff !important; }
.t-TreeNav .a-TreeView-label { color: inherit !important; }
/* Page heading (Static Content) */
.km-page-head { display: flex; flex-direction: column; gap: 2px; margin: 4px 2px 2px; }
.km-page-head h1 { margin: 0; font-size: 24px; font-weight: 800; color: var(--km-ink); }
.km-page-head p { margin: 0; color: var(--km-muted); font-size: 14px; }
.km-greet { display: flex; flex-direction: column; margin: 2px 2px 0; }
.km-greet__hello { font-size: 13px; color: var(--km-muted); font-weight: 600; }
.km-greet__name { margin: 2px 0; font-size: 24px; line-height: 1.2; font-weight: 800; color: var(--km-ink); }
.km-greet__sub { font-size: 14px; color: var(--km-muted); }
.km-text-danger { color: var(--km-red) !important; }
/* Section region (Standard template, no UI): title as a section heading */
.km-section, .km-section > .t-Region-bodyWrap, .km-section .t-Region-body { background: transparent !important; border: 0 !important; box-shadow: none !important; }
.km-section > .t-Region-header { background: transparent; border: 0; padding: 4px 2px 8px; }
.km-section .t-Region-title { font-size: 16px; font-weight: 800; color: var(--km-ink); }
.km-section .t-Region-body { padding: 0 !important; }
/* Generic regions */
.t-Region { border-radius: var(--km-radius); border: 0; box-shadow: var(--km-shadow); }
.t-Region-header { background: #fff; border-bottom: 1px solid var(--km-line); border-radius: var(--km-radius) var(--km-radius) 0 0; }
.t-Region-title { font-weight: 800; color: var(--km-ink); }
/* Action rows (Buttons Container, no UI) */
.km-actions, .km-actions .t-ButtonRegion { background: transparent !important; box-shadow: none !important; border: 0 !important; padding: 0 !important; }
.km-actions--center .t-ButtonRegion-wrap { justify-content: center; }
.km-actions--center .t-ButtonRegion-col--left:empty { display: none; }
/* ===== Buttons ================================================================================ */
.t-Button { border-radius: 12px; font-weight: 700; }
.t-Button--hot, .t-Button.t-Button--hot { background: var(--km-grad); border: 0; color: #fff; box-shadow: 0 8px 18px rgba(106, 74, 240, .28); }
.t-Button--hot:hover { filter: brightness(1.05); color: #fff; }
.t-Button:not(.t-Button--hot):not(.t-Button--header):not(.t-Button--noUI) { background: #fff; border: 1px solid var(--km-field-border); color: var(--km-ink); box-shadow: none; }
/* ===== Forms (.km-form on a Standard region) =================================================== */
.km-form .t-Region-bodyWrap { display: flex; flex-direction: column; }
.km-form .t-Region-body { order: 1; padding-bottom: 6px; }
.km-form .t-Region-buttons--top { order: 2; margin: 0; padding: 14px 16px 18px; border-top: 1px solid var(--km-line); }
.km-form .t-Region-buttons--bottom { order: 3; }
.km-form .t-Region-buttons--top:not(:has(.t-Button)) { display: none; }
.km-form .t-Region-buttons-left, .km-form .t-Region-buttons-right { display: flex; gap: 8px; flex-wrap: wrap; }
.km-narrow { max-width: 680px; }
.t-Form-inputContainer input.text_field, .t-Form-inputContainer input.password, .t-Form-inputContainer input.number_field,
.t-Form-inputContainer select, .t-Form-inputContainer textarea, .t-Form-inputContainer .apex-item-text {
border-radius: 12px; border-color: var(--km-field-border); background: var(--km-field); min-height: 42px; }
.t-Form-inputContainer :is(input, select, textarea):focus { border-color: var(--km-primary); box-shadow: 0 0 0 3px rgba(124, 92, 252, .18); background: #fff; }
.t-Form-label { font-weight: 700; color: var(--km-ink); }
.t-Form-inlineHelp, .t-Form-helpText { color: var(--km-muted); font-size: 12px; }
.t-Form-itemWrapper > .a-Button { border-radius: 0 12px 12px 0 !important; border: 1px solid var(--km-field-border); border-left: 0; background: var(--km-primary-50); color: var(--km-primary-600); }
.t-Form-itemWrapper > input:has(+ .a-Button), .t-Form-itemWrapper > .apex-item-text:has(+ .a-Button) { border-top-right-radius: 0 !important; border-bottom-right-radius: 0 !important; }
.apex-item-option label { font-weight: 600; }
/* ===== Cards regions (a-CardView) ============================================================== */
.km-cards { gap: 14px !important; margin: 0 0 18px !important; }
.km-cards .a-CardView-header { border-bottom: 0 !important; }
.km-cards .a-CardView-headerBody { padding-inline: 0 !important; }
.km-greet, .km-page-head { margin-bottom: 14px !important; }
.km-tiles { margin-bottom: 18px; }
.km-cards .a-CardView { border: 0; border-radius: var(--km-radius); background: var(--km-card); box-shadow: var(--km-shadow); color: var(--km-ink);
transition: transform .15s ease, box-shadow .15s ease; }
.km-cards .a-CardView.has-actions--full:hover { transform: translateY(-2px); box-shadow: var(--km-shadow-hover); }
.km-cards .a-CardView-header { padding: 16px 16px 6px; gap: 12px; align-items: flex-start; }
.km-cards .a-CardView-title { font-size: 15.5px; font-weight: 800; color: var(--km-ink); }
.km-cards .a-CardView-subTitle { font-size: 12.5px; color: var(--km-muted); font-weight: 600; margin-top: 2px; }
.km-cards .a-CardView-body { padding: 4px 16px 16px; }
.km-cards .a-CardView-mainContent, .km-cards .a-CardView-subContent { font-size: 13px; color: var(--km-ink); }
.km-cards .a-CardView-initials { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 13px;
background: var(--km-primary-50) !important; color: var(--km-primary-600) !important; font-weight: 800; font-size: 14px; }
.km-cards .a-CardView-icon { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 13px; background: var(--km-teal-bg); color: var(--km-teal); font-size: 18px; }
.km-cards .a-CardView-badge { align-self: flex-start; }
.km-cards .a-CardView-actions { padding: 0 16px 14px; border: 0; }
.km-cards .a-CardView-button { border-radius: 10px; font-weight: 800; }
.km-cards .a-CardView-button--hot { background: var(--km-grad); color: #fff; border: 0; }
.a-CardView-items--grid1col.km-cards, .km-cards.a-CardView-items { padding: 0; }
.km-cards + .a-GV-noDataMsg, .km-section .a-CardView-noDataMsg, .a-CardView-noDataMsg { padding: 22px; border-radius: var(--km-radius); background: rgba(255, 255, 255, .7); color: var(--km-muted); text-align: center; }
/* List style: rows inside one white card, separated by lines */
.km-cards--list { gap: 0 !important; padding: 6px !important; background: var(--km-card); border-radius: var(--km-radius); box-shadow: var(--km-shadow); }
.km-cards--list .a-CardView { box-shadow: none; border-radius: 14px; display: flex; flex-direction: row !important; align-items: center; }
.km-cards--list .a-CardView-header { border: 0 !important; }
.km-cards--list .a-CardView-item + .a-CardView-item { border-top: 1px solid var(--km-line); }
.km-cards--list .a-CardView.has-actions--full:hover { transform: none; background: var(--km-primary-50); box-shadow: none; }
.km-cards--list .a-CardView-header { flex: 1; min-width: 0; padding: 12px 10px; align-items: center; }
.km-cards--list .a-CardView-headerBody { min-width: 0; flex: 1; }
.km-cards--list .a-CardView-title { font-size: 14.5px; }
.km-cards--list .a-CardView-badge { align-self: center; }
.km-cards--list .a-CardView-body { padding: 0 12px 0 0; flex: 0 0 auto !important; width: auto; }
.km-cards--list .a-CardView-mainContent { font-weight: 800; text-align: right; white-space: nowrap; }
.km-cards--list .a-CardView-actions { padding: 0 10px 0 0; }
.km-card--out .a-CardView-mainContent { color: var(--km-red); }
.km-card--in .a-CardView-mainContent { color: var(--km-green); }
.km-card--me { background: var(--km-primary-50) !important; }
.km-cards--turns .a-CardView-initials { border-radius: 50%; background: var(--km-grad) !important; color: #fff !important; box-shadow: 0 6px 14px rgba(106, 74, 240, .25); }
.km-cards--context { box-shadow: none; background: var(--km-primary-50); }
.km-cards--context .a-CardView { background: transparent; }
@media (max-width: 640px) {
.km-cards--list .a-CardView { flex-wrap: wrap; }
.km-cards--list .a-CardView-body { padding: 0 12px 10px 62px; width: 100%; }
.km-cards--list .a-CardView-mainContent { text-align: left; }
.km-cards--list .a-CardView-actions { padding: 0 12px 12px 62px; }
}
/* Grid cards (Kamatis, members, my Kamatis) */
.km-cards--grid .a-CardView-body { display: flex; flex-direction: column; gap: 10px; }
.km-cards--people .a-CardView-mainContent { color: var(--km-muted); font-weight: 600; font-size: 12.5px; }
.km-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--km-muted); font-weight: 600; }
.km-foot { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; color: var(--km-muted); font-weight: 700; }
.km-myturn { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 14px; background: var(--km-primary-50); margin-bottom: 8px; }
.km-myturn strong { display: block; font-size: 14px; }
.km-myturn small { font-size: 12px; color: var(--km-muted); }
.km-turn { display: grid; place-items: center; flex-shrink: 0; width: 36px; height: 36px; border-radius: 50%; background: var(--km-grad); color: #fff; font-weight: 800; font-size: 14px; box-shadow: 0 6px 14px rgba(106, 74, 240, .25); }
/* Gradient balance card */
.km-cards--balance .a-CardView { position: relative; overflow: hidden; background: var(--km-grad) !important; color: #fff; box-shadow: var(--km-shadow-lg); border-radius: 22px; }
.km-cards--balance .a-CardView:hover { transform: none; }
.km-cards--balance .a-CardView-header { padding: 22px 22px 0; }
.km-cards--balance .a-CardView-headerBody { padding: 0; }
.km-cards--balance .a-CardView-body { padding: 4px 22px 22px; }
.km-cards--balance .a-CardView-mainContent { display: flex; flex-direction: column; gap: 4px; color: #fff; }
.km-balance__label { font-size: 13px; opacity: .85; font-weight: 600; color: #fff; }
.km-balance__amount { font-size: 32px; font-weight: 800; letter-spacing: -.5px; }
.km-balance__meta { font-size: 12.5px; opacity: .85; max-width: 75%; }
.km-balance__wave { position: absolute; right: 22px; top: 22px; width: 96px; height: 40px; }
.km-balance__wave path { fill: none; stroke: #5eead4; stroke-width: 4; stroke-linecap: round; }
.km-progress { height: 8px; border-radius: 99px; background: var(--km-primary-50); overflow: hidden; }
.km-progress > span { display: block; height: 100%; border-radius: inherit; background: var(--km-grad); }
.km-progress--light { background: rgba(255, 255, 255, .25); margin-top: 10px; max-width: 70%; }
.km-progress--light > span { background: #fff; }
/* Stat cards */
.km-cards--stats { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
@media (min-width: 720px) { .km-cards--stats { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } }
.km-cards--stats .a-CardView { background: rgba(255, 255, 255, .72); box-shadow: none; border: 1px solid #fff; }
.km-cards--stats .a-CardView-header { padding: 14px 16px; }
.km-cards--stats .a-CardView-title { font-size: 19px; }
.km-cards--stats .a-CardView-subTitle { font-size: 12px; }
.km-cards--stats .km-stat--alert .a-CardView-title { color: var(--km-red); }
/* Detail header (hero) card */
.km-cards--hero .a-CardView { border-radius: 22px; }
.km-cards--hero .a-CardView:hover { transform: none; }
.km-cards--hero .a-CardView-header { padding: 22px 22px 8px; }
.km-cards--hero .a-CardView-title { font-size: 22px; }
.km-cards--hero .a-CardView-body { padding: 8px 22px 22px; display: flex; flex-direction: column; gap: 14px; }
.km-facts { display: grid; gap: 12px; grid-template-columns: repeat(2, 1fr); }
@media (min-width: 720px) { .km-facts { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .km-facts { grid-template-columns: repeat(6, 1fr); } }
.km-facts > div { padding: 12px; border-radius: 14px; background: #faf9ff; }
.km-facts small { display: block; color: var(--km-muted); font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px; }
.km-facts strong { font-size: 15px; font-weight: 800; }
.km-hero-progress { padding: 14px; border-radius: 14px; background: var(--km-grad); color: #fff; font-size: 13px; font-weight: 700; }
.km-hero-progress .km-progress--light { max-width: none; }
.km-hint { padding: 12px 14px; border-radius: 14px; background: var(--km-primary-50); color: var(--km-primary-600); font-size: 13px; font-weight: 700; }
.km-hint--warn { background: var(--km-amber-bg); color: #9a5a00; }
/* Payout banner card */
.km-cards--banner .a-CardView { display: flex; flex-direction: row !important; align-items: center; background: var(--km-grad) !important; color: #fff; box-shadow: var(--km-shadow-lg); }
.km-cards--banner .a-CardView-header { flex: 1; padding: 16px; align-items: center; }
.km-cards--banner .a-CardView-title, .km-cards--banner .a-CardView-subTitle { color: #fff !important; }
.km-cards--banner .a-CardView-subTitle { opacity: .85; }
.km-cards--banner .a-CardView-icon { background: rgba(255, 255, 255, .18); color: #fff; }
.km-cards--banner .a-CardView-actions { padding: 0 16px 0 0; }
.km-cards--banner .a-CardView-button { background: #fff !important; color: var(--km-primary-600) !important; border: 0; box-shadow: none; }
/* Account status card */
.km-cards--status { max-width: 540px; margin: 40px auto 8px !important; }
.km-cards--status .a-CardView { padding: 30px 26px; text-align: center; border-radius: 24px; }
.km-cards--status .a-CardView:hover { transform: none; }
.km-cards--status .a-CardView-header { flex-direction: column; align-items: center; padding: 0; }
.km-cards--status .a-CardView-icon { width: 64px; height: 64px; border-radius: 20px; font-size: 28px; background: var(--km-primary-50); color: var(--km-primary); }
.km-cards--status .a-CardView-title { font-size: 22px; margin-top: 10px; }
.km-cards--status .a-CardView-body { padding: 8px 0 0; }
.km-cards--status .a-CardView-mainContent { color: var(--km-muted); font-size: 14.5px; line-height: 1.6; }
/* ===== Badges & pills ============================================================================ */
.km-badge { display: inline-block; padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 800; letter-spacing: .2px; white-space: nowrap; border: 0; }
.km-badge--green { background: var(--km-green-bg); color: var(--km-green); }
.km-badge--amber { background: var(--km-amber-bg); color: var(--km-amber); }
.km-badge--red { background: var(--km-red-bg); color: var(--km-red); }
.km-badge--blue { background: var(--km-blue-bg); color: var(--km-blue); }
.km-badge--violet { background: var(--km-primary-50); color: var(--km-primary-600); }
.km-badge--grey { background: var(--km-grey-bg); color: var(--km-grey); }
.km-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; background: var(--km-grey-bg); color: var(--km-grey); font-size: 11px; font-weight: 700; }
.km-pill--ok { background: var(--km-green-bg); color: var(--km-green); }
/* ===== Quick-action tiles (List region, Cards list template) ======================================= */
.km-tiles .t-Cards { display: grid !important; gap: 12px; grid-template-columns: repeat(2, 1fr); margin: 0; }
@media (min-width: 720px) { .km-tiles .t-Cards { grid-template-columns: repeat(4, 1fr); } }
.km-tiles .t-Cards-item { width: auto !important; padding: 0 !important; margin: 0 !important; }
.km-tiles .t-Card { height: 100%; border: 0; border-radius: var(--km-radius); overflow: hidden; background: #fff; box-shadow: var(--km-shadow); transition: transform .15s ease, box-shadow .15s ease; }
.km-tiles .t-Card-wrap { border-radius: var(--km-radius); background: #fff; }
.km-tiles .t-Card:hover { transform: translateY(-2px); box-shadow: var(--km-shadow-hover); }
.km-tiles .t-Card-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 104px; padding: 16px 10px; text-decoration: none; }
.km-tiles .t-Card-icon { position: static; display: grid; place-items: center; width: 46px; height: 46px; margin: 0; border-radius: 14px; font-size: 20px; }
.km-tiles .t-Card-icon .t-Icon { color: inherit; }
.km-tiles .t-Card-titleWrap { padding: 0; text-align: center; }
.km-tiles .t-Card-title { font-size: 13px; font-weight: 800; color: var(--km-ink); margin: 0; }
.km-tiles .t-Card-subtitle, .km-tiles .t-Card-body, .km-tiles .t-Card-colorFill { display: none; }
.km-tiles .t-Cards-item:nth-child(4n+1) .t-Card-icon { background: var(--km-primary-50); color: var(--km-primary); }
.km-tiles .t-Cards-item:nth-child(4n+2) .t-Card-icon { background: var(--km-pink-bg); color: var(--km-pink); }
.km-tiles .t-Cards-item:nth-child(4n+3) .t-Card-icon { background: var(--km-amber-bg); color: var(--km-amber); }
.km-tiles .t-Cards-item:nth-child(4n+4) .t-Card-icon { background: var(--km-teal-bg); color: var(--km-teal); }
.km-tone--violet { background: var(--km-primary-50); color: var(--km-primary); }
.km-tone--pink { background: var(--km-pink-bg); color: var(--km-pink); }
.km-tone--amber { background: var(--km-amber-bg); color: var(--km-amber); }
.km-tone--teal { background: var(--km-teal-bg); color: var(--km-teal); }
/* ===== Faceted Search (search box + filters) ======================================================= */
.km-search { margin-bottom: 4px; }
.km-search .a-FS-search, .km-search .apex-item-search, .km-search input[type="search"] { border-radius: 14px !important; border-color: var(--km-field-border) !important; background: #fff !important; min-height: 44px; box-shadow: var(--km-shadow); }
.km-search .a-FS { display: flex; flex-direction: column; gap: 12px; padding: 0; background: transparent; border: 0; }
/* Each facet = one row: label, then chips */
.km-search .a-FS-control { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; padding: 0; border: 0 !important; background: transparent; }
.km-search .a-FS-header { display: flex; align-items: center; gap: 8px; padding: 0; border: 0; background: transparent; }
.km-search .a-FS-label { margin: 0; font-size: 12px; font-weight: 800; color: var(--km-muted); text-transform: uppercase; letter-spacing: .4px; }
.km-search .a-FS-clearButton { padding: 2px 8px; font-size: 12px; font-weight: 700; color: var(--km-primary); background: transparent; border: 0; }
.km-search .a-FS-body, .km-search .a-FS-wrap, .km-search .a-FS-bodyInner { padding: 0; border: 0; max-height: none; overflow: visible; background: transparent; }
.km-search .a-FS-body { flex: 1 1 auto; min-width: 0; }
.km-search .apex-item-checkbox { flex-direction: row !important; }
.km-search .apex-item-option { width: auto !important; flex: 0 0 auto; }
.km-search .apex-item-checkbox, .km-search .apex-item-group--rc { display: flex !important; flex-wrap: wrap; gap: 8px; }
.km-search .apex-item-option { margin: 0; padding: 0; }
.km-search .apex-item-option label { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 7px 14px; border-radius: 999px; background: #fff;
box-shadow: var(--km-shadow); font-size: 13px; font-weight: 700; color: var(--km-ink); cursor: pointer; }
.km-search .apex-item-option label::before, .km-search .apex-item-option label::after { display: none !important; }
.km-search .apex-item-option[aria-selected="true"] label { background: var(--km-grad); color: #fff; }
.km-search .apex-item-option.is-disabled { display: none; }
.km-search .apex-item-option-badge { padding: 0 7px; border-radius: 999px; background: var(--km-primary-50); color: var(--km-primary-600); font-size: 11px; font-weight: 800; }
.km-search .apex-item-option[aria-selected="true"] .apex-item-option-badge { background: rgba(255, 255, 255, .25); color: #fff; }
.km-search .a-FS-listFooter { display: none; }
/* ===== Phone bottom navigation (List region, Links List template) =================================== */
.km-bnav { display: none; }
@media (max-width: 640px) {
.km-bnav { position: fixed; z-index: 900; left: 12px; right: 12px; bottom: 12px; display: block; padding: 8px 6px calc(8px + env(safe-area-inset-bottom));
border-radius: 22px; background: rgba(255, 255, 255, .96); box-shadow: 0 10px 30px rgba(30, 27, 58, .14); backdrop-filter: blur(10px); }
.km-bnav .t-LinksList { display: flex; justify-content: space-around; margin: 0; padding: 0; border: 0; }
.km-bnav .t-LinksList-item { border: 0; flex: 1; }
.km-bnav .t-LinksList-link { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 6px 4px; border-radius: 14px; color: var(--km-muted); font-size: 10.5px; font-weight: 700; text-align: center; }
.km-bnav .t-LinksList-icon { margin: 0; }
.km-bnav .t-LinksList-icon .t-Icon { font-size: 18px; color: inherit; }
.km-bnav .t-LinksList-badge { display: none; }
.km-bnav .t-LinksList-item.is-current > .t-LinksList-link { background: var(--km-primary); color: #fff; box-shadow: 0 8px 16px rgba(124, 92, 252, .35); }
.t-Body-content { padding-bottom: 96px; }
.t-Header-controls .t-Button--headerTree { display: none; }
.t-Body-content { padding-left: 12px; padding-right: 12px; }
.km-balance__amount { font-size: 28px; }
.km-greet__name { font-size: 21px; }
.km-form .t-Region-buttons--top { display: flex; flex-direction: column-reverse; gap: 10px; }
.km-form .t-Region-buttons-left, .km-form .t-Region-buttons-right { flex-direction: column; width: 100%; }
.km-form .t-Region-buttons--top .t-Button { width: 100%; justify-content: center; min-height: 46px; }
}
/* ===== Sign in / register ========================================================================= */
/* Full-height page without the header bar; two equal-height columns centred in a 1120px container. */
.km-auth-page { --ut-header-height: 0px; }
.km-auth-page .t-Header { display: none; }
.km-auth-page .t-Body, .km-auth-page .t-Body-main { padding-top: 0 !important; margin-top: 0 !important; }
.km-auth-page .t-Body-contentInner { max-width: 1120px; min-height: 100vh; margin: 0 auto; padding: 32px 24px !important;
display: flex; flex-direction: column; justify-content: center; }
.km-auth-page .t-Body-contentInner > .container { width: 100%; }
.km-auth-page .t-Body-contentInner .row { align-items: stretch; }
.km-auth-page .t-Body-contentInner .col { display: flex; flex-direction: column; }
.km-auth-col { flex: 1 1 auto; margin-bottom: 0 !important; }
/* Left: illustration panel */
.km-auth-col--hero > .km-auth-hero { height: 100%; }
.km-auth-hero { display: flex; flex-direction: column; gap: 22px; padding: 36px; border-radius: 28px; border: 1px solid #fff;
background: linear-gradient(160deg, #fdf0fa 0%, #efeaff 55%, #e6f1ff 100%); box-shadow: var(--km-shadow); }
.km-auth-hero h1 { margin: 0; font-size: 34px; line-height: 1.18; font-weight: 400; color: var(--km-ink); letter-spacing: -.3px; }
.km-auth-hero h1 b { font-weight: 800; }
.km-auth-hero p { margin: 10px 0 0; color: var(--km-muted); line-height: 1.6; font-size: 15px; max-width: 460px; }
.km-auth-points { margin: auto 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 8px; }
.km-auth-points li { display: inline-flex; align-items: center; gap: 6px; padding: 7px 12px; border-radius: 999px; background: rgba(255, 255, 255, .8);
font-size: 12.5px; font-weight: 700; color: var(--km-ink); }
.km-auth-points .fa { color: var(--km-primary); }
.km-brand { display: flex; align-items: center; gap: 8px; font-size: 22px; font-weight: 800; color: var(--km-ink); }
.km-brand__mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 11px 11px 11px 4px; background: var(--km-grad); color: #fff; font-size: 18px; }
.km-phone { align-self: center; width: 240px; padding: 12px; border-radius: 30px; background: #fff; box-shadow: 0 30px 60px rgba(106, 74, 240, .18); }
.km-phone__screen { display: flex; flex-direction: column; gap: 8px; padding: 10px; border-radius: 22px; background: #fbfaff; }
.km-phone__screen small { font-size: 10px; color: var(--km-muted); }
.km-phone__screen > strong { font-size: 13px; margin-top: -6px; }
.km-mini-balance { display: flex; flex-direction: column; padding: 12px; border-radius: 14px; background: var(--km-grad); color: #fff; font-size: 10px; box-shadow: 0 8px 18px rgba(106, 74, 240, .3); }
.km-mini-balance b { font-size: 17px; }
.km-mini-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.km-mini-tiles span { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px; border-radius: 12px; background: #fff; font-size: 10px; font-weight: 700; box-shadow: var(--km-shadow); }
.km-mini-tiles i { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; font-size: 12px; }
/* Right: sign-in card */
.km-auth-card { border-radius: 28px !important; border: 1px solid #fff !important; box-shadow: 0 24px 60px rgba(106, 74, 240, .14) !important; }
.km-auth-card > .t-Region-bodyWrap { height: 100%; justify-content: center; }
.km-auth-card .t-Region-body { padding: 36px 36px 8px !important; }
.km-auth-card__brand { display: none; align-items: center; gap: 8px; margin-bottom: 18px; font-size: 20px; font-weight: 800; }
.km-auth-title { margin: 0 0 6px; font-size: 28px; font-weight: 800; color: var(--km-ink); letter-spacing: -.3px; }
.km-auth-sub { margin: 0 0 22px; color: var(--km-muted); font-size: 14.5px; line-height: 1.5; }
.km-auth-card .t-Form-fieldContainer { margin-bottom: 4px; }
.km-auth-card .t-Form-inputContainer input { min-height: 48px; font-size: 15px; }
.km-auth-card .t-Region-buttons--top { display: flex !important; flex-direction: column !important; gap: 16px; border-top: 0 !important; padding: 8px 36px 36px !important; }
.km-auth-card .t-Region-buttons-right { order: 1; width: 100%; flex-direction: column !important; }
.km-auth-card .t-Region-buttons-left, .km-auth-card .t-Region-buttons-right { margin: 0 !important; padding: 0 !important; }
.km-auth-card .t-Region-buttons-right .t-Button { width: 100%; justify-content: center; min-height: 50px; font-size: 15.5px; border-radius: 14px; }
.km-auth-card .t-Region-buttons-left { order: 2; width: 100%; display: grid !important; grid-template-columns: 1fr 1fr; gap: 10px; }
.km-auth-card .t-Region-buttons-left:not(:has(.t-Button)) { display: none !important; }
.km-auth-card .t-Region-buttons-left::before { content: "or explore with demo data"; grid-column: 1 / -1; display: block; padding: 2px 0 4px; text-align: center;
font-size: 12px; font-weight: 700; color: var(--km-muted);
background: linear-gradient(var(--km-line), var(--km-line)) left center / 26% 1px no-repeat,
linear-gradient(var(--km-line), var(--km-line)) right center / 26% 1px no-repeat; }
.km-auth-card .km-demo-btn { width: 100%; justify-content: center; min-height: 46px; border-radius: 14px;
background: var(--km-primary-50) !important; border: 1px dashed #cfc2ff !important; color: var(--km-primary-600) !important; font-weight: 800; }
.km-auth-card .km-demo-btn:hover { background: #e9e3ff !important; }
.km-auth-card .km-demo-btn .t-Icon { color: inherit; }
.km-auth-foot { margin: 16px 0 0; text-align: center; font-size: 14px; color: var(--km-muted); }
.km-auth-link { color: var(--km-primary); font-weight: 800; text-decoration: none; }
.km-auth-link:hover { text-decoration: underline; }
.km-loading { padding: 40px; text-align: center; color: var(--km-muted); }
/* Tablet: illustration simplified; phone: card only, with its own brand line */
@media (max-width: 1000px) { .km-phone { display: none; } .km-auth-hero { justify-content: center; } }
@media (max-width: 640px) {
.km-auth-col--hero { display: none; }
.km-auth-page .t-Body-contentInner { padding: 20px 14px !important; justify-content: flex-start; }
.km-auth-card__brand { display: flex; }
.km-auth-card .t-Region-body { padding: 26px 22px 6px !important; }
.km-auth-card .t-Region-buttons--top { padding: 6px 22px 26px !important; }
.km-auth-title { font-size: 24px; }
}
/* Alerts */
.t-Alert--success, .t-Body-alert .t-Alert { border-radius: 14px; }
Comments
Post a Comment