/* Zen-style vertical tabs: a sidebar column with the terminal inset to its right. Every color here is a name from `palette.zig`, which writes the matching `@define-color` block for the scheme in force; `appearance.zig` prepends that and loads the pair as a single provider. Nothing below may hardcode a color — a literal hex would be a rule that looks right in one scheme and wrong in the other, it would look right in whichever one you happened to be testing in, and the color editor in the settings page could not reach it at all. `alpha()` is used for every wash rather than a second named color, so a state's tint is always derived from the state's own color and the two can't drift apart. */ .playpen-window { background-color: @pp_bg; } /* The sidebar is a card, not a column: rounded on every corner and inset from the window edge, so it sits on the backdrop the same way a pane does. It used to run edge to edge and meet the content at a hairline, which left the top-left corner reading as three unrelated things sharing one point — the window edge, the header buttons and the first tab row all at once. */ .playpen-sidebar { background-color: @pp_sidebar; border: 1px solid @pp_border; border-radius: 12px; margin: 6px 0 6px 6px; } /* Room above the buttons so they clear the card's rounded top edge rather than sitting in it. The rule underneath is stated explicitly rather than left to the one Adwaita draws for every header bar: that one is a shadow in the desktop theme's color, which on this palette read as a stray line rather than as the pair it now makes with the footer's. */ .playpen-sidebar headerbar { background: none; box-shadow: none; min-height: 44px; padding: 6px 8px 2px 8px; border-bottom: 1px solid @pp_border; } /* The two header actions. Styled explicitly rather than left to Adwaita, whose default button is a gray that belongs to a different window than this one. Scoped by class instead of by `headerbar button` so the window controls beside them keep their own appearance — those belong to the desktop, not to the app, and making them match would be the wrong kind of consistent. Two selectors because a MenuButton wraps its button in a `menubutton` node, so the class lands one level above the thing that draws. */ button.playpen-header-button, .playpen-header-button > button { min-width: 30px; min-height: 30px; padding: 0; margin: 0 1px; border-radius: 8px; color: @pp_text_dim; background-color: @pp_surface_raised; background-image: none; border: 1px solid @pp_border; box-shadow: none; } button.playpen-header-button:hover, .playpen-header-button > button:hover { background-color: @pp_row_hover; background-image: none; color: @pp_text; } /* The strip below the tab list, holding the settings button. Separated by a rule so the button doesn't read as a row of the list it sits under. */ .playpen-sidebar-footer { padding: 6px 8px; border-top: 1px solid @pp_border; } /* The restore-session offer, between the tab list and the footer. Rules of its own rather than a reused row style: it is not a tab, and styling it as one would put a fifth thing in a column of four that all select something. Bordered top and bottom so it reads as a band across the sidebar rather than a card floating in it — the footer below already draws the same hairline, and the pair of them frame the offer without adding a third weight. The accent tint is what makes it the one thing in the column asking to be clicked, kept low enough that it does not compete with the selected row above it. */ .playpen-restore { padding: 6px 8px; border-top: 1px solid @pp_border; background-color: alpha(@pp_accent_strong, 0.08); } .playpen-restore-action { padding: 5px 6px; border-radius: 8px; color: @pp_text_dim; } .playpen-restore-action:hover { background-color: alpha(@pp_accent_strong, 0.16); color: @pp_text; } /* The one accent-colored thing in the banner, so that what draws the eye is the symbol for the action rather than the sentence describing it. */ .playpen-restore-icon { color: @pp_accent_strong; } .playpen-restore-title { font-weight: bold; font-size: 0.92em; } /* How many tabs are on offer, sized and dimmed like the sublabels in the settings dialog: a caption on the action above it, not a second action. */ .playpen-restore-detail { color: @pp_text_faint; font-size: 0.82em; } /* Quieter than the action beside it, and quieter still than a tab's close button, which at least closes something you were looking at. Saying no to this should be easy to find and hard to hit by accident. */ .playpen-restore-dismiss { min-width: 22px; min-height: 22px; padding: 0; color: @pp_text_faint; } .playpen-restore-dismiss:hover { color: @pp_text; } .playpen-settings-button { min-width: 26px; min-height: 26px; padding: 0; color: @pp_text_faint; } .playpen-settings-button:hover { color: @pp_text; } .playpen-list { background: none; padding: 4px 8px; } .playpen-list > row { border-radius: 8px; margin: 2px 0; padding: 7px 8px; color: @pp_text_dim; transition: background-color 120ms ease; } .playpen-list > row:hover { background-color: @pp_row_hover; } .playpen-list > row:selected { background-color: @pp_row_selected; color: @pp_text; } /* The kind icon picks up the accent on the current row. The status dot is excluded: it carries a color that *is* the information, and this rule is specific enough to beat the state classes, which used to leave the selected row's dot accent-colored whatever it was trying to say. */ .playpen-list > row:selected image:not(.playpen-status-dot) { color: @pp_accent_strong; } /* An emoji standing in for that icon. Sized to the width the icon occupies rather than left to the glyph, so a sidebar mixing emoji rows with icon rows still starts every label in the same column — emoji vary in advance width and without this the labels beside them would step in and out by a pixel or two per row. The font size is a little under the icon's 16px because a color glyph fills its box where a symbolic icon leaves air around itself. */ .playpen-tab-emoji { min-width: 16px; font-size: 13px; } /* State on the row itself, not just on its dot. One bar and two washes, and that is the whole vocabulary. Finished gets the bar down its leading edge; the two states that actually want you get a wash of their own color behind the row. The dot says what a row is doing once you are reading it; these are what let you sweep a sidebar of a dozen tabs and land on the one you care about without reading any of them. Only finished carries a bar. Every state used to have one, drawn as an inset shadow, and a shadow is clipped to the row's 8px corners: what landed was a short bar with a curl at each end, four rows deep in slightly different colors. Read as ornament rather than as signal, so it is gone from everything but the one state whose bar is a plain rectangle. These come after the :selected rules deliberately. Equal specificity means source order decides, and a state outranks "this is the tab you are on". */ /* Working is the dot alone. It is the most common state by far, and a sidebar where half the rows are marked all afternoon teaches you to stop looking. */ /* No wash for finished, for a different reason: a tinted row is what selection looks like in this list, so washing the finished ones left "the tab you are on" and "the tab that just finished" saying the same thing. The dot and the bar are enough — nothing here is waiting on you. The bar is a background image rather than an inset shadow precisely because it is not clipped to the corners, so it stays a rectangle at any height. Tall enough now to read as a deliberate mark rather than a stray pixel, and still short of the row's full height so it reads as pinned to the middle of the row. */ .playpen-list > row.playpen-attn-done { background-image: linear-gradient(@pp_ok, @pp_ok); background-size: 3px 18px; background-position: left center; background-repeat: no-repeat; } .playpen-list > row.playpen-attn-input { background-color: alpha(@pp_warn, 0.14); } .playpen-list > row.playpen-attn-failed { background-color: alpha(@pp_err, 0.14); } /* The current row keeps its own background: the wash and the selection would otherwise blend into a color that reads as neither, and the dot is left to carry the state on its own — which on the row you are already looking at is enough. Done is absent because it has no wash to suppress, only a bar, and a bar and a selection sit together fine. */ .playpen-list > row:selected.playpen-attn-input, .playpen-list > row:selected.playpen-attn-failed { background-color: @pp_row_selected; } /* The row being dragged, left dimmed and accented where it currently sits. As with a pane there is no separate drop indicator: the sidebar reorders live during the drag, so the list itself is the preview and this only has to say which row is the one on the move. Last of the row rules, so it wins over both the selection and the state washes. While you are moving a tab, where it is going is the only thing you are looking for. */ .playpen-list > row.dragging { opacity: 0.65; background-color: alpha(@pp_accent_strong, 0.18); background-image: none; } /* The copy of that row traveling with the pointer, which GTK draws into a surface of its own. What it paints is the row's *box* — the icon, the label and the close button — and a row's background belongs to the row above that box, so without this the label would float on nothing. Given the selected row's background it reads as the row lifted off the list, which is what it is. Qualified by a class the window puts on this drag's icon, because the `dnd` node belongs to every drag icon in the app: unqualified, this would also repaint the one a pane drag carries. */ dnd.playpen-tab-drag { background-color: @pp_row_selected; color: @pp_text; border-radius: 8px; } /* Keep the close button unobtrusive until the row is hovered or current. */ .playpen-close { opacity: 0; min-width: 20px; min-height: 20px; padding: 0; } .playpen-list > row:hover .playpen-close, .playpen-list > row:selected .playpen-close { opacity: 0.65; } .playpen-close:hover { opacity: 1; } .playpen-content { background-color: @pp_bg; } /* A view is the container for one tab's terminals. The left side has no padding of its own: the root split's separator is the 6px gutter there, so the content stays evenly inset while that gap doubles as the drag handle that resizes the sidebar. */ .playpen-view { padding: 6px 6px 6px 0; } /* Paned itself draws nothing; the panes inside it carry the styling. */ .playpen-view paned { background: none; } /* Each terminal sits in its own rounded frame so multiple panes in a view read as distinct surfaces. */ .playpen-pane { background-color: @pp_surface; border-radius: 10px; border: 1px solid @pp_border; } .playpen-pane.active { border-color: @pp_accent_muted; } .playpen-pane-header { padding: 2px 4px 2px 10px; background-color: @pp_surface_raised; border-bottom: 1px solid @pp_border; } .playpen-pane.active .playpen-pane-header { background-color: @pp_surface_raised_active; } .playpen-pane-title { font-size: 0.82em; color: @pp_text_faint; } .playpen-pane.active .playpen-pane-title { color: @pp_text; } .playpen-pane-button { min-width: 22px; min-height: 22px; padding: 0; opacity: 0; } .playpen-pane:hover .playpen-pane-button, .playpen-pane.active .playpen-pane-button { opacity: 0.55; } .playpen-pane-button:hover { opacity: 1; } .playpen-pane-icon { color: @pp_text_faint; -gtk-icon-size: 14px; } .playpen-pane.active .playpen-pane-icon { color: @pp_accent_strong; } /* Status dots. One shared appearance for the sidebar row and the pane header, colored entirely by which state class is on the icon. Symbolic icons take their color from `color`, so nothing here needs a second asset. These are read at a glance from across a screen full of tabs, so the colors are picked to survive that: amber and red carry the two states that actually want you, and they are the only warm colors anywhere in the window. */ .playpen-status-dot { -gtk-icon-size: 11px; opacity: 0; } .playpen-status-busy, .playpen-status-done, .playpen-status-input, .playpen-status-failed { opacity: 1; } /* Working: blue, and pulsing. Its own palette color rather than the accent — the accent is whatever the theme is built around, and a green one put "still working" in the same color as "finished", which is the one distinction this whole indicator exists to make. The pulse carries the rest of it: a mark that moves reads as something in progress before you have registered its color. */ .playpen-status-busy { color: @pp_busy; animation: playpen-pulse 1.6s ease-in-out infinite; } /* Finished while you were elsewhere. Green, and still — the dot that has stopped moving is the one that has stopped working. */ .playpen-status-done { color: @pp_ok; } /* Blocked on you. Amber, and not animated — a pulsing dot reads as progress, and this is the opposite of progress. */ .playpen-status-input { color: @pp_warn; } .playpen-status-failed { color: @pp_err; } @keyframes playpen-pulse { 0% { opacity: 0.35; } 50% { opacity: 1; } 100% { opacity: 0.35; } } /* Renaming a tab: a single entry in a popover hanging off the row. */ .playpen-rename { padding: 8px; min-width: 220px; } /* The row's right-click menu. Narrower than the rename popover and with tighter padding: it holds two words per line, and a menu sized like a form reads as though something failed to fill it. */ .playpen-row-menu { padding: 4px; min-width: 150px; } .playpen-row-menu > button { padding: 5px 8px; border-radius: 6px; } /* The rule between what a row *is* and whether it is allowed to interrupt you. Inset from the popover's edges so it reads as a divider inside a list rather than as the list being cut in two. */ .playpen-row-menu > separator { margin: 3px 6px; background-color: @pp_border; } /* What a tab's mute is currently doing, above the ways out of it. Sized and dimmed like the sublabels in the settings dialog, because it is the same kind of thing: a caption on the controls under it, not a control. */ .playpen-row-menu-state { padding: 2px 8px; color: @pp_text_faint; font-size: 0.85em; } /* A web pane's navigation bar, below the pane header. Kept visually quieter than the header so the two rows don't compete. */ .playpen-nav { padding: 4px 6px; background-color: @pp_surface; border-bottom: 1px solid @pp_border; } .playpen-nav-button { min-width: 24px; min-height: 24px; padding: 0; color: @pp_text_dim; } .playpen-nav-button:disabled { opacity: 0.3; } .playpen-nav-entry { min-height: 24px; margin-left: 4px; padding: 2px 8px; border-radius: 6px; font-size: 0.85em; background-color: @pp_bg; color: @pp_text; border: 1px solid @pp_border; box-shadow: none; } .playpen-nav-entry:focus-within { border-color: @pp_accent; } /* Find-in-page, in the same clothes as the nav bar above it: it is the same kind of furniture, just one that comes and goes. */ .playpen-find { padding: 4px 6px; background-color: @pp_surface; border-bottom: 1px solid @pp_border; } /* A search with no results says so in the box's border rather than only in the count beside it, which is easy to miss mid-typing. */ .playpen-find-entry.error { border-color: @pp_err; } .playpen-find-status { margin: 0 4px; font-size: 0.8em; color: @pp_text_dim; } /* The pane currently filling its tab. The other panes are still open and still running, just not drawn, so this is marked quietly — a lit border rather than anything alarming. The header's toggle icon carries the rest of the message. */ .playpen-pane.zoomed { border-color: @pp_accent; } .playpen-pane.zoomed .playpen-pane-button { opacity: 0.55; } /* State on a pane's own frame, so a tab full of splits says which pane is asking without being read pane by pane. Only the three states that are news get a border. Working deliberately does not: it is the resting state of every pane you have set going, and coloring the frame for it would leave the whole view repainting itself all afternoon while saying nothing you did not already know. Its pulsing dot is enough. After `.active` and `.zoomed` in source order, so a state outranks both — they say where you are, which you know, and this says what happened, which you don't. `.dragging` still wins over all of it, further down: while you are moving a pane, feedback about the move is the only thing that matters. */ .playpen-pane.playpen-attn-done { border-color: @pp_ok; } .playpen-pane.playpen-attn-input { border-color: @pp_warn; } .playpen-pane.playpen-attn-failed { border-color: @pp_err; } /* The header carries a wash of the same color. The border alone is a hairline around a large shape; the header is a solid band right next to the dot and the title, which is what makes a pane readable at a glance in a four-way split. */ .playpen-pane.playpen-attn-done .playpen-pane-header { background-color: alpha(@pp_ok, 0.13); } .playpen-pane.playpen-attn-input .playpen-pane-header { background-color: alpha(@pp_warn, 0.13); } .playpen-pane.playpen-attn-failed .playpen-pane-header { background-color: alpha(@pp_err, 0.13); } /* The pane being dragged. There is no separate drop indicator: the layout rearranges live during the drag, so the view itself is the preview. */ .playpen-pane.dragging { opacity: 0.65; border-color: @pp_accent_strong; } /* Saved-layout menu, hanging off the sidebar header. */ .playpen-layout-menu { min-width: 260px; } .playpen-layout-menu button { padding: 4px 8px; border-radius: 6px; } .playpen-layout-empty { padding: 8px; color: @pp_text_faint; font-size: 0.9em; } /* Layout dialogs: opening one, and saving the current tab as one. */ .playpen-dialog { background-color: @pp_surface; } .playpen-dialog-content { padding: 16px; } .playpen-dialog-actions { padding: 12px 16px; border-top: 1px solid @pp_border; } .playpen-dialog-heading { margin-top: 8px; font-weight: bold; color: @pp_text; } .playpen-dialog-label { color: @pp_text_dim; } .playpen-dialog-sublabel { font-size: 0.88em; color: @pp_text_faint; } .playpen-dialog-hint { font-size: 0.85em; color: @pp_text_faint; } .playpen-dialog-error { color: @pp_err; } /* A directory parameter's field, and the completions hanging under it. The list is a hint rather than a menu — it never takes the focus — so it is styled quietly: the surface it sits on, and rows that only stand out when they are the one the arrow keys have landed on. */ .playpen-path-list contents { padding: 4px; min-width: 240px; background-color: @pp_surface_raised; } .playpen-path-list list { background-color: transparent; } .playpen-path-list row { border-radius: 6px; } .playpen-path-row { padding: 3px 8px; color: @pp_text; } .playpen-path-more { padding: 3px 8px; font-size: 0.85em; color: @pp_text_faint; } /* Each captured pane in the save dialog, so the sections read apart. */ .playpen-dialog-pane { padding: 8px; border: 1px solid @pp_border; border-radius: 8px; } /* ------------------------------------------------------------------------- Settings One section per group of preferences, each a titled card on the dialog's surface. There is a single group today; the card is what stops the second one from needing a redesign. */ .playpen-settings-group { padding: 12px 14px; border: 1px solid @pp_border; border-radius: 10px; background-color: @pp_surface_raised; } .playpen-settings-title { font-weight: bold; color: @pp_text; } /* The scheme picker: three linked buttons rather than a dropdown, so the whole choice is visible without opening anything, and so the one you are on is readable at a glance rather than by reading a word. */ /* `background-image: none` on every rule below is load-bearing. Adwaita paints button states with a gradient *over* the background color, so setting the color alone leaves a washed-out version of it — most visibly on the checked button, where the accent came out as a pale gray-blue. `:backdrop` is spelled out for the same reason: Adwaita dims unfocused windows, and the selected scheme is exactly the thing that has to stay readable while you are looking at the window behind this one to see what it did. */ .playpen-settings-choice > button { padding: 5px 14px; color: @pp_text_dim; background-color: @pp_surface; background-image: none; border: 1px solid @pp_border; box-shadow: none; } .playpen-settings-choice > button:hover { background-color: @pp_row_hover; background-image: none; color: @pp_text; } .playpen-settings-choice > button:checked, .playpen-settings-choice > button:checked:backdrop { background-color: @pp_accent; background-image: none; border-color: @pp_accent; /* Against a filled accent in either scheme — the light palette's accent is dark enough that white text is the readable choice there too. */ color: #ffffff; } .playpen-settings-choice > button:checked:hover { background-color: @pp_accent_strong; background-image: none; border-color: @pp_accent_strong; } /* ------------------------------------------------------------------------- The color editor One row per color in the palette, in sections. The rows are deliberately plain: on a page where thirty-nine swatches are the point, the swatch should be the only thing on a line carrying any color, and a bordered card per section would read as noise. The space between sections does that work instead. */ .playpen-color-group { margin-top: 2px; } .playpen-color-group-title { font-size: 0.9em; font-weight: bold; color: @pp_text_dim; } /* The value beside the swatch, so a palette can be read off and copied out as text rather than only poked at through a picker. */ .playpen-color-hex { font-family: monospace; font-size: 0.85em; color: @pp_text_faint; } /* Adwaita gives a color button the full button treatment: a gradient over the color, and a border in a gray belonging to neither palette. What is wanted is a rectangle of the color with this app's border around it, so the swatch reads as the color rather than as a button holding one. */ .playpen-color-group colorbutton > button { padding: 0; min-width: 0; min-height: 0; background-image: none; box-shadow: none; border: 1px solid @pp_border; border-radius: 6px; } .playpen-color-group colorbutton > button:hover { border-color: @pp_border_strong; } /* The contrast slider, for the same reason as the swatch above it: Adwaita paints a scale in the *desktop's* accent color, which on a page whose whole subject is the color this window is painted from would be the one thing on screen ignoring it. Left stock, dragging it toward a warm theme leaves a blue bar sitting in the middle of the result. */ .playpen-contrast trough { background-color: @pp_surface_raised_active; } .playpen-contrast slider { background-color: @pp_accent; border: 1px solid @pp_accent_strong; box-shadow: none; } .playpen-contrast:disabled slider { background-color: @pp_border_strong; border-color: @pp_border_strong; } /* The tick at the neutral position, and the two words at the ends. Faint on purpose: they label the ends of a scale whose real readout is the window behind the dialog. */ .playpen-contrast marks indicator { background-color: @pp_border_strong; } .playpen-contrast marks label { font-size: 0.8em; color: @pp_text_faint; } /* ------------------------------------------------------------------------- The startup list One card per tab the app opens itself with. The border is doing real work: a row's parameter fields sit *under* its header line, and without something drawing the boundary they read as belonging to the row below them instead. */ .playpen-startup-row { padding: 8px; border: 1px solid @pp_border; border-radius: 8px; } /* Indented under the header line, which is the other half of the same grouping: these fields are a property of the layout picked above them. */ .playpen-startup-params { margin-left: 10px; } /* ------------------------------------------------------------------------- The emoji picker A tab's chosen glyph, then a search box over a grid of every glyph on offer. The grid is a GtkFlowBox, so each cell is a `flowboxchild` wrapping a label and the hover and selection states have to be drawn here — Adwaita styles a flow box for lists of cards, which at this size leaves the cells looking like nothing in particular. */ /* The current choice, big enough to check from across the dialog. Boxed to the same square whether it holds a glyph or the word "None", so switching between them doesn't shift the text beside it. */ .playpen-emoji-preview { min-width: 44px; min-height: 44px; font-size: 26px; border: 1px solid @pp_border; border-radius: 10px; background-color: @pp_surface; } .playpen-emoji-preview-none { font-size: 0.85em; color: @pp_text_faint; } /* Sunk into the dialog rather than raised off it: the grid is a well you look into, and the section card it sits in is already the raised surface. */ .playpen-emoji-scroller { border: 1px solid @pp_border; border-radius: 10px; background-color: @pp_surface; } .playpen-emoji-grid { padding: 6px; background: none; } .playpen-emoji-cell { font-size: 19px; } .playpen-emoji-grid > flowboxchild { padding: 0; border-radius: 8px; background: none; transition: background-color 100ms ease; } .playpen-emoji-grid > flowboxchild:hover { background-color: @pp_row_hover; } /* The glyph in force. `background-image: none` for the same reason as the theme buttons above: Adwaita paints selection with a gradient over the color, and without this the accent comes out washed. */ .playpen-emoji-grid > flowboxchild:selected, .playpen-emoji-grid > flowboxchild:selected:backdrop { background-color: @pp_accent_muted; background-image: none; box-shadow: inset 0 0 0 1px @pp_accent; } /* The divider between the sidebar and the content: the gutter that was always there, made draggable. It paints as the backdrop so nothing looks different until it is hovered, the same way the pane dividers behave. */ .playpen-root-split > separator { background-color: @pp_bg; min-width: 6px; } .playpen-root-split > separator:hover { background-color: @pp_accent_muted; } /* Divider between panes. Wide enough to grab without hunting for it. */ .playpen-view paned > separator { background-color: @pp_bg; min-width: 6px; min-height: 6px; } .playpen-view paned > separator:hover { background-color: @pp_accent_muted; }