Files
decePubClient/Styles/neo.css
T
thepraandClaude Opus 5.5 6731077191 Dialogs close from a button and rise from the bottom on phones
DModal draws a close button (✕, named by CloseText) beside its heading, in a header that stays in view while the box
scrolls; a long list of who liked a post left only the thin margins around the box to tap out of it. A new
ResponsiveBottom placement makes the box a sheet from the bottom below md and keeps it in the middle from md; the
report, liked-by and boosted-by, edit history and public notice dialogs use it. Boxes are at most 100dvh tall, so a
phone's browser bars don't cut them, and a bottom sheet leaves 3rem above itself and clears the home indicator. The
report dialog gets a Cancel button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-06 22:43:35 +02:00

368 lines
13 KiB
CSS

/* Neumorphism on top of daisyUI.
Each element gets two shadows: a light one up-left and a dark one down-right, or both inset for a pressed look. The
colours are the theme's --neo-light / --neo-dark (theme.css); the offset and blur are --neo-o / --neo-b.
- neo-raised / neo-inset / neo-flat draw the shadows. neo-2xs, neo-xs (the default), neo-sm and neo-md set the
size, and Tailwind variants work on them (md:neo-sm).
- neo-gradient / neo-gradient-pressed are the 145deg gradients raised and pressed controls use.
- neo-hue-track turns a daisyUI range into the rainbow hue slider.
The daisyUI components are restyled with @utility <component>, as daisyUI recommends. Tailwind emits those rules
straight into the utilities layer, while daisyUI's own rules sit in sublayers of it (daisyui.l1.l2.l3 base,
l1.l2 modifiers, l1 states, daisyui disabled). So these overrides beat every daisyUI rule, variants included, and
each one excludes the variants it must leave alone. :where() keeps those exclusions at specificity (0,1,0).
To drop a neo shadow at a call site use shadow-none! or neo-flat!.
A gradient on a control is painted from its border box (background-origin: border-box). daisyUI keeps a 1px border
on its controls, made transparent here, and a background image starts at the padding box and repeats into the
border: the strip then showed the far end of the gradient, a thin seam along the edges, as if the control sat in a
second frame.
Never write @utility collapse: it brings back Tailwind's own collapse utility (visibility: collapse), which daisyUI
otherwise suppresses, and hides every collapse. DCollapse adds neo-raised in its markup instead. */
/* Not inherited, so a child doesn't pick up a parent's neo-sm size. */
@property --neo-o {
syntax: "<length>";
inherits: false;
initial-value: 3px;
}
@property --neo-b {
syntax: "<length>";
inherits: false;
initial-value: 6px;
}
@utility neo-raised {
box-shadow: calc(-1 * var(--neo-o)) calc(-1 * var(--neo-o)) var(--neo-b) var(--neo-light), var(--neo-o) var(--neo-o) var(--neo-b) var(--neo-dark);
}
@utility neo-inset {
box-shadow: inset var(--neo-o) var(--neo-o) var(--neo-b) var(--neo-dark), inset calc(-1 * var(--neo-o)) calc(-1 * var(--neo-o)) var(--neo-b) var(--neo-light);
}
@utility neo-flat {
box-shadow: none;
}
@utility neo-2xs {
--neo-o: 2px;
--neo-b: 4px;
}
@utility neo-xs {
--neo-o: 3px;
--neo-b: 6px;
}
@utility neo-sm {
--neo-o: 6px;
--neo-b: 12px;
}
@utility neo-md {
--neo-o: 8px;
--neo-b: 16px;
}
@utility neo-gradient {
background-image: linear-gradient(145deg, var(--neo-grad-light), var(--neo-grad-dark));
background-origin: border-box;
}
@utility neo-gradient-pressed {
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
}
@utility neo-hue-track {
--range-fill: 0;
--range-bg: transparent;
&::-webkit-slider-runnable-track {
background-image: linear-gradient(to right, hsl(0 90% 80%), hsl(60 90% 80%), hsl(120 90% 80%), hsl(180 90% 80%), hsl(240 90% 80%), hsl(300 90% 80%), hsl(359 90% 80%));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&::-moz-range-track {
background-image: linear-gradient(to right, hsl(0 90% 80%), hsl(60 90% 80%), hsl(120 90% 80%), hsl(180 90% 80%), hsl(240 90% 80%), hsl(300 90% 80%), hsl(359 90% 80%));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
/* Buttons: raised, with a gradient built from whatever colour the button has; pressed (active, checked) they sink in
with the gradient reversed. The original neoBtn* classes had no hover state, so daisyUI's hover tint is dropped. */
@utility btn {
&:not(:where(.btn-ghost, .btn-link, .btn-outline, .btn-dash, .btn-soft, .btn-disabled, :disabled, [disabled], [aria-disabled="true"])) {
--btn-bg: var(--btn-color, var(--color-base-100));
--btn-border: transparent;
background-image: linear-gradient(145deg, color-mix(in oklab, var(--btn-bg), white var(--neo-lift)), color-mix(in oklab, var(--btn-bg), black var(--neo-sink)));
background-origin: border-box;
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
&:where(:focus-visible) {
background-image: linear-gradient(-45deg, color-mix(in oklab, var(--btn-bg), white var(--neo-lift)), color-mix(in oklab, var(--btn-bg), black var(--neo-sink)));
}
&:where(.btn-sm, .btn-xs) {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
&:where(:active, .btn-active, :checked, [aria-pressed="true"], [aria-current="page"]) {
background-image: linear-gradient(145deg, color-mix(in oklab, var(--btn-bg), black var(--neo-sink)), color-mix(in oklab, var(--btn-bg), white var(--neo-lift)));
box-shadow: inset 3px 3px 6px var(--neo-dark), inset -3px -3px 6px var(--neo-light);
translate: none;
}
}
}
/* Text fields: a pressed well, darker while focused, with no focus ring and no border unless a colour or a failed
validation asks for one.
Fields (input, select, file-input, textarea) take the boxes' radius (--radius-box, .75rem). daisyUI gives them
--radius-field (2rem), which stays the buttons': a pill on a one-line field, and on a textarea a curve that ate into
its first and last lines. daisyUI reads the variable, so a rounded-* class at the call site (the composer's) still
wins. */
@utility input {
--radius-field: var(--radius-box);
&:not(:where(.input-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
background-image: linear-gradient(145deg, var(--neo-grad-darker), var(--neo-grad-lighter));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
&:not(:where(.input-ghost, .input-neutral, .input-primary, .input-secondary, .input-accent, .input-info, .input-success, .input-warning, .input-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
/* daisyUI draws the select's arrow with background-image, so the well here is the background colour alone. */
@utility select {
--radius-field: var(--radius-box);
&:not(:where(.select-ghost)) {
background-color: var(--color-base-100);
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
box-shadow: inset 3px 3px 6px var(--neo-dark), inset -3px -3px 6px var(--neo-light);
}
}
&:not(:where(.select-ghost, .select-neutral, .select-primary, .select-secondary, .select-accent, .select-info, .select-success, .select-warning, .select-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
/* label.select: the inner select inherits the label's background and pulls itself .75rem to the left, which
daisyUI expects a span.label to absorb. Let the well show through and give any other prefix (an icon) the same
room. */
& > select {
background-color: transparent;
}
& > :first-child:not(select, .label) {
flex-shrink: 0;
margin-inline-end: .75rem;
}
}
@utility textarea {
--radius-field: var(--radius-box);
&:not(:where(.textarea-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&:focus, &:focus-within {
outline: none;
background-image: linear-gradient(145deg, var(--neo-grad-darker), var(--neo-grad-lighter));
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
&:not(:where(.textarea-ghost, .textarea-neutral, .textarea-primary, .textarea-secondary, .textarea-accent, .textarea-info, .textarea-success, .textarea-warning, .textarea-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
@utility file-input {
--radius-field: var(--radius-box);
&:not(:where(.file-input-ghost)) {
background-color: var(--color-base-100);
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&:not(:where(.file-input-ghost, .file-input-neutral, .file-input-primary, .file-input-secondary, .file-input-accent, .file-input-info, .file-input-success, .file-input-warning, .file-input-error, .validator:user-invalid, [aria-invalid="true"])) {
border-color: transparent;
}
}
/* Selectors: an inset well while unchecked; once checked they keep daisyUI's filled look. */
@utility checkbox {
&:not(:where(:checked, :indeterminate, [aria-checked="true"], [aria-checked="mixed"], :disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility radio {
&:not(:where(:checked, [aria-checked="true"], :disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility toggle {
&:not(:where(:disabled)) {
border-color: transparent;
background-image: linear-gradient(145deg, var(--neo-grad-dark), var(--neo-grad-light));
background-origin: border-box;
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
&::before {
box-shadow: -1px -1px 2px var(--neo-light), 1px 1px 2px var(--neo-dark);
}
}
}
@utility range {
&::-webkit-slider-runnable-track {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
&::-moz-range-track {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
/* Surfaces: raised like the old .neomorph panels. */
@utility card {
&:not(:where(.card-border, .card-dash)) {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
}
/* At most the screen's height as the phone shows it (dvh: its browser bars come and go), and a bottom sheet leaves room
above it and clears the home indicator. */
@utility modal-box {
box-shadow: -6px -6px 12px var(--neo-light), 6px 6px 12px var(--neo-dark);
max-height: 100dvh;
:where(.modal-bottom) > & {
max-height: calc(100dvh - 3rem);
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
}
}
@utility dropdown-content {
background-color: var(--color-base-100);
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
/* A display utility on the content (flex, grid) beats daisyUI's display:none on a closed dropdown, which then stays
invisible (opacity 0) but still in the way of the pointer, over whatever it covers: hide it from the pointer too. */
:where(.dropdown.dropdown-close, .dropdown:not(.dropdown-open, .dropdown-hover:hover, :focus-within)) & {
visibility: hidden;
}
}
@utility alert {
&:not(:where(.alert-outline, .alert-dash, .alert-soft)) {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
}
@utility stats {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility timeline-box {
border-color: transparent;
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
@utility badge {
&:not(:where(.badge-ghost, .badge-outline, .badge-dash, .badge-soft)) {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
}
@utility kbd {
box-shadow: -2px -2px 4px var(--neo-light), 2px 2px 4px var(--neo-dark);
}
@utility tabs-box {
background-color: var(--color-base-100);
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
@utility chat-bubble {
box-shadow: 2px 2px 4px var(--neo-dark);
}
@utility dock {
box-shadow: 0 -3px 6px var(--neo-light), 0 -1px 3px var(--neo-dark);
}
/* The active menu entry is pressed in rather than filled with the neutral colour. */
@utility menu {
--menu-active-bg: transparent;
--menu-active-fg: var(--color-base-content);
& :where(li > .menu-active, li > [aria-current="page"]) {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
/* Wells: tracks, placeholders and the OTP digit boxes sink in. */
@utility otp {
& > span {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility progress {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
@utility skeleton {
&:not(:where(.skeleton-text)) {
box-shadow: inset 2px 2px 4px var(--neo-dark), inset -2px -2px 4px var(--neo-light);
}
}
@utility mockup-browser {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility mockup-window {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility mockup-phone {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}
@utility mockup-code {
box-shadow: -3px -3px 6px var(--neo-light), 3px 3px 6px var(--neo-dark);
}